/* 
    WhaleSpotter
    Copyright (C) 2007  Bob Mottram
    fuzzgun@gmail.com

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using sluggish.utilities;

namespace whalespotter
{
    public partial class frmMain : Form
    {
        const String VERSION = "1.05";

        //current position of the mouse within an image
        Point LocalMousePosition;
        ArrayList manual_points = new ArrayList();
        bool manual_keypoints = false;

        // This is the base directory where all images and maps are stored
        // change this as needed
        const String base_path = "C:\\whalespotter\\";

        private int Stride;

        // maps
        whaleMaps maps = new whaleMaps();  // stores map data
        private String maps_filename = base_path + "maps.dat";
        private String current_map_filename = "";
        private String prev_map_filename = "";

        private bool useForIdentifications;
        private String current_sighting_filename = "";

        private float whale_icon_size = 0.03f;  // size to draw whale icons

        // number of bins used for comparing tail edge patterns
        private const int EDGE_PATTERN_BINS = 60;
        private bool[] edge_pattern;
        private bool compareEdgePattern;  // compare the tail edge pattern or not, depending upon the tilt angle

        // colour of the whale icon on the map
        private int whale_icon_r = 0;
        private int whale_icon_g = 0;
        private int whale_icon_b = 255;

        // program settings
        settings userSettings = new settings();
        String user_settings_filename;

        whale findwhale;

        // database storing whale sightings
        whaleDatabase database = new whaleDatabase();
        private String database_filename = System.AppDomain.CurrentDomain.BaseDirectory + "\\whales.dat";
        
        // filename used for spreadsheet export
        private String spreadsheet_export_filename = System.AppDomain.CurrentDomain.BaseDirectory + "\\whales.csv";

        // path where standardised tail images are stored
        private String tailPath = base_path + "tails\\";
        String tailFilename;

        int min_percent_coverage = 20;     // the minimum percentage of the tail which should be visible for recognition
        bool segmentation_ok = false;      // was the segmentation ok?

        int max_intensity_threshold = 180;
        int min_intensity_threshold = 100;
        float inconsistency_threshold = 3.5f;
        int normal_V_angle = 130;  // the normal V angle of the tail when vertically oriented

        // dimensions of the standardised tail image
        int tail_image_width = 500;
        int tail_image_height = 400;
        int percentCoverage = 0;

        // dimensions of the map image
        int map_image_width = 0;
        int map_image_height = 0;

        //int intensity_threshold = 180;
        int blue_threshold = 60;
        int green_threshold = 40;
        float total_inconsistency = 0;
        int line_width = 1;
        int selected_search_result_index = 0;

        // background colour used on standardised tail image
        int tail_image_background_r = 180;
        int tail_image_background_g = 180;
        int tail_image_background_b = 255;

        int image_width=0, image_height=0;
        int horizontal_profile_max, vertical_profile_max;
        Byte[] raw_image = null;
        Byte[] foreground = null;
        Byte[] keypoints = null;
        Byte[] tail_image = null;
        Byte[] map_image = null;
        Byte[] tail_features = null;
        int[,,] tail_image_coords = null;
        int[,,] tail_index = null;
        Byte[] edges_image = null;
        bool[, ,] sillouette = null;
        Byte[] profiles = null;
        int[] vertical_profile;
        int[] vertical_profile_smoothed;
        int[] horizontal_profile;

        // bounding box of the tail
        int foreground_tx, foreground_ty, foreground_bx, foreground_by;

        int Texture = 0;
        int prev_d_leading=-1, prev_d_trailing = -1;
        float prev_coordinate_x = -1;
        int tail_width = 0;            // width from tip to tip
        int tail_axis_x = 0;           // tail centre x position
        int tail_axis_y = 0;           // tail centre x position
        int tail_left_x = 0;           // left tail tip x position
        int tail_left_y = 0;           // left tail tip y position
        int tail_right_x = 0;          // right tail tip x position
        int tail_right_y = 0;          // right tail tip y position
        int tail_base_left_x = 0;      // point at which te left side of the tail contacts the water surface
        int tail_base_left_y = 0;      // point at which te left side of the tail contacts the water surface
        int tail_base_right_x = 0;     // point at which te right side of the tail contacts the water surface
        int tail_base_right_y = 0;     // point at which te right side of the tail contacts the water surface
        int tail_base_x, tail_base_y;  // position of the base of the tail
        float tail_roll = 0;           // roll angle
        float tail_tilt = 0;           // tilt angle relative to the camera
        float tail_pan = 0;            // pan angle relative to the camera
        int tail_left_x2, tail_left_y2, tail_right_x2, tail_right_y2;
        bool leading_edge_length_unknown = false;
        String ImageFilename = "";
        ArrayList tail_left = new ArrayList();       // points along the trailing left side of the tail
        ArrayList tail_right = new ArrayList();      // points along the trailing right side of the tail
        ArrayList tail_left_side = new ArrayList();  // points along the leading left side of the tail
        ArrayList tail_right_side = new ArrayList(); // points along the leading right side of the tail
        ArrayList key_features = new ArrayList();    // stores the locations of key features used for recognition
        ArrayList LinearFeatures = new ArrayList();  // stores the locations of key features used for recognition
        ArrayList LinearFeaturesAll = new ArrayList();  // stores the locations of key features used for recognition

        ArrayList search_results;
        ArrayList search_indexes = null;

        #region "image processing"

        /// <summary>
        /// make an image of the tail in standard coordinates
        /// </summary>
        private void createTailImage()
        {
            int tail_base_height = tail_image_height * 80 / 100;
            int raw_img_x = 0, raw_img_y = 0;

            // this image stores a standardised picture of the tail
            tail_image = new Byte[tail_image_width * tail_image_height * 3];

            // this image shows detected features on the standardised tail
            tail_features = new Byte[tail_image_width * tail_image_height * 3];

            // this array stores standardised coordinates, for quick lookup
            tail_image_coords = new int[tail_image_width, tail_image_height, 2];

            // this is the edge image of the standardised tail
            edges_image = new Byte[tail_image_width * tail_image_height * 3];

            // this is an index which stores numbers indicating whether
            // each tail segment in a 20x10 array contains predominantly black or white pixels
            // it's used to perform a quick initial lookup when searching
            tail_index = new int[20, 2, 3];

            // set the tail image to a background colour
            for (int i = 0; i < tail_image_width * tail_image_height * 3; i += 3)
            {
                tail_image[i] = (Byte)tail_image_background_b;
                tail_image[i + 1] = (Byte)tail_image_background_g;
                tail_image[i + 2] = (Byte)tail_image_background_r;
            }

            int tx = (tail_image_width / 2) - (tail_image_width / 10);
            int ty = tail_base_height;
            int bx = tx;
            int by = tail_image_height - 1;
            showLine(tail_image, tail_image_width, tail_image_height, tx, ty, bx, by, 0, 0, 0, false);
            tx = (tail_image_width / 2) + (tail_image_width / 10);
            bx = tx;
            showLine(tail_image, tail_image_width, tail_image_height, tx, ty, bx, by, 0, 0, 0, false);

            int average_intensity = 0;
            int hits = 0;
            int min_intensity = 255;
            int max_intensity_left = 0;
            int max_intensity_right = 0;
            percentCoverage = 0;
            int percentCoverageMax = 0;
            for (float x = -100; x <= 100; x += 0.2f)
            {
                for (float y = 0; y <= 100; y += 0.2f)
                {
                    percentCoverageMax++;
                    int std_img_x = 0, std_img_y = 0;
                    getTailImageCoordinate(x, y, ref std_img_x, ref std_img_y);
                    int n = ((std_img_y * tail_image_width) + std_img_x) * 3;

                    getTailCoordinate(x, y, ref raw_img_x, ref raw_img_y, false);
                    if ((raw_img_x > -1) && (raw_img_y > -1) && (std_img_y > -1))
                    {
                        percentCoverage++;

                        tail_image_coords[std_img_x, std_img_y, 0] = (int)x;
                        tail_image_coords[std_img_x, std_img_y, 1] = (int)y;

                        int n2 = (raw_img_y * Stride) + (raw_img_x * 3);
                        int intensity = 0;
                        for (int col = 0; col < 3; col++)
                        {
                            tail_image[n + col] = raw_image[n2 + col];
                            intensity += raw_image[n2 + col];
                        }
                        intensity /= 3;

                        // update the tail index
                        if (intensity > 110)
                            tail_index[(int)((x + 100) / 10), (int)y / 50, 0]++;
                        else
                            tail_index[(int)((x + 100) / 10), (int)y / 50, 1]++;

                        if (x < 0)
                        {
                            if (intensity > max_intensity_left) max_intensity_left = intensity;
                        }
                        else
                        {
                            if (intensity > max_intensity_right) max_intensity_right = intensity;
                        }
                        if (intensity < min_intensity) min_intensity = intensity;
                        average_intensity += intensity;
                        hits++;
                        //edges_image[n + 2] = (Byte)intensity;
                        //edges_image[n] = 0;
                    }
                    else
                    {
                        if ((std_img_y > -1) && ((y < 1) || (y > 99)))
                        {
                            for (int col = 0; col < 3; col++)
                                tail_image[n + col] = 0;
                        }
                    }
                }
            }

            percentCoverage = percentCoverage * 100 / percentCoverageMax;

            if (hits > 0)
            {
                // update the tail index
                for (int x = 0; x < 20; x++)
                {
                    for (int y = 0; y < 2; y++)
                    {
                        if ((tail_index[x, y, 1] > 0) || (tail_index[x, y, 0] > 0))
                        {
                            // notice that if the white percent is close to 50/50
                            // then we leave the index value as indeterminate
                            int whitePercent = tail_index[x, y, 0] * 100 / (tail_index[x, y, 1] + tail_index[x, y, 0]);
                            tail_index[x, y, 0] = whitePercent;             // percentage of white pixels
                            tail_index[x, y, 1] = 100 - whitePercent;       // percentage of black pixels
                            if (whitePercent > 70)
                                tail_index[x, y, 2] = 2; // this is a white segment
                            else
                            {
                                if (whitePercent < 30)
                                    tail_index[x, y, 2] = 1; // this is a black segment
                                else
                                    tail_index[x, y, 2] = 0;  // indeterminate
                            }
                        }
                    }
                }

                // Enhance intensity
                // The intensity of pixels in the standardised are increased
                // relative to the original raw image, to make any small
                // features more visible
                float intensity_multiplier_left = 0.3f + (255.0f / max_intensity_left);
                float intensity_multiplier_right = 0.3f + (255.0f / max_intensity_right);

                for (int x = 0; x < tail_image_width; x++)
                {
                    for (int y = 0; y < tail_image_height * 80 / 100; y++)
                    {
                        int n = ((y * tail_image_width) + x) * 3;
                        if ((tail_image[n] != tail_image_background_b) &&
                            (tail_image[n + 1] != tail_image_background_g))
                        {
                            if (x < tail_image_width / 2)
                            {
                                for (int col = 0; col < 3; col++)
                                {
                                    int newval = (int)(tail_image[n + col] * intensity_multiplier_left);
                                    if (newval > 255) newval = 255;
                                    tail_image[n + col] = (Byte)newval;
                                }
                            }
                            else
                            {
                                for (int col = 0; col < 3; col++)
                                {
                                    int newval = (int)(tail_image[n + col] * intensity_multiplier_right);
                                    if (newval > 255) newval = 255;
                                    tail_image[n + col] = (Byte)newval;
                                }
                            }
                        }
                    }
                }


                // create an edges image, which can subsequently be used to find
                // interesting features
                createEdgeImage(tail_image, edges_image, tail_image_width, tail_image_height, ref Texture);
            }

            // copy into the features image
            for (int i = 0; i < tail_image_width * tail_image_height * 3; i++)
                tail_features[i] = tail_image[i];

        }

        /// <summary>
        /// make an edges image
        /// </summary>
        /// <param name="source_img">standardised image of the tail</param>
        /// <param name="edge_img">edge image</param>
        /// <param name="img_width">image width</param>
        /// <param name="img_height">image height</param>
        /// <param name="TextureValue">texture</param>
        private void createEdgeImage(Byte[] source_img, Byte[] edge_img, int img_width, int img_height,
                                     ref int TextureValue)
        {
            int tail_base_height = img_height * 80 / 100;

            // create an edges image, which can subsequently be used to find
            // interesting features
            int average_intensity_left = 0;
            int average_intensity_right = 0;
            int hits_left = 0;
            int hits_right = 0;
            int radius = 3;
            TextureValue = 0;
            for (int x = radius; x < img_width - radius; x++)
            {
                for (int y = radius; y < tail_base_height - radius; y++)
                {
                    int n = ((y * img_width) + x) * 3;
                    if ((source_img[n] != tail_image_background_b) &&
                        (source_img[n + 1] != tail_image_background_g))
                    {
                        int leftval = 0;
                        int rightval = 0;
                        for (int n2 = n - (radius * 3); n2 < n + (radius * 3); n2++)
                        {
                            if (n2 < n)
                                leftval += source_img[n2];
                            else
                                rightval += source_img[n2];
                        }
                        for (int n2 = n - (img_width * radius * 3); n2 < n + (img_width * radius * 3); n2 += (img_width * 3))
                        {
                            if (n2 < n)
                                leftval += source_img[n2];
                            else
                                rightval += source_img[n2];
                        }
                        int edgeval = (rightval - leftval) / (radius * 2 * 3);
                        if (edgeval < 0) edgeval = -edgeval;
                        //edgeval = (int)(edgeval * multiplier);
                        if (edgeval > 255) edgeval = 255;

                        if (x < img_width / 2)
                        {
                            average_intensity_left += edgeval;
                            hits_left++;
                        }
                        else
                        {
                            average_intensity_right += edgeval;
                            hits_right++;
                        }

                        for (int col = 0; col < 3; col++)
                        {
                            edge_img[n + col] = (Byte)edgeval;
                        }
                    }
                }
            }

            // Show only high intensity edges
            // Note that different thresholds are used for the left and right
            // sides of the tail.  This helps to counteract any shadowing effects
            if (hits_left > 0) average_intensity_left /= hits_left;
            if (hits_right > 0) average_intensity_right /= hits_right;
            average_intensity_left *= 210 / 100;
            average_intensity_right *= 210 / 100;
            int TextureHits = 0;
            for (int x = radius; x < img_width - radius; x++)
            {
                for (int y = radius; y < tail_base_height - radius; y++)
                {
                    int n = ((y * img_width) + x) * 3;
                    if ((source_img[n] != tail_image_background_b) &&
                        (source_img[n + 1] != tail_image_background_g))
                    {
                        int intensity = edge_img[n];
                        if (x < img_width / 2)
                        {
                            if (intensity > average_intensity_left)
                                intensity = 255;
                            else
                                intensity = 0;
                        }
                        else
                        {
                            if (intensity > average_intensity_right)
                                intensity = 255;
                            else
                                intensity = 0;
                        }

                        if (intensity == 255) TextureValue++;
                        TextureHits++;

                        for (int col = 0; col < 3; col++)
                            edge_img[n + col] = (Byte)intensity;
                    }
                }
            }

            if (TextureHits > 0) TextureValue = TextureValue * 1000 / TextureHits;

        }



        /// <summary>
        /// locate the tips of the tail
        /// </summary>
        private void locateTailTips()
        {
            tail_left_x = -1;
            tail_right_x = -1;
            for (int x = 0; x < image_width; x++)
            {
                if (vertical_profile_smoothed[x] > 0)
                {
                    tail_right_x = x;
                    if (tail_left_x == -1)
                        tail_left_x = x;
                }
            }

            int min_y = image_height / 20;
            int neighbourhood = tail_width / 30;
            tail_left_y = image_height;
            tail_right_y = image_height;
            int axis_y = 0;
            int search_x = neighbourhood;
            int prev_value = 1;
            int prev_value2 = 1;
            for (int x = -search_x; x <= search_x; x++)
            {
                if ((tail_left_x + x > -1) && (tail_left_x + x < image_width - 3))
                {
                    axis_y = image_height;
                    int ymax = tail_axis_y + (image_height / 30);
                    if (ymax >= image_height) ymax = image_height - 1;
                    for (int y = ymax; y > min_y; y--)
                    {
                        int n = (y * Stride) + ((tail_left_x + x) * 3);
                        int n2 = (y * Stride) + ((tail_left_x + x + 2) * 3);
                        int n3 = ((y + 2) * Stride) + ((tail_left_x + x) * 3);
                        if ((horizontal_profile[y] > 2) && (foreground[n] == 0) && (foreground[n3] == 0) && (foreground[n2] == 0) && ((prev_value == 0) || (prev_value2 == 0)))
                        {
                            axis_y = y;
                        }
                        prev_value2 = prev_value;
                        prev_value = foreground[n];
                    }
                    if (axis_y < tail_left_y) tail_left_y = axis_y;
                }
            }

            prev_value = 1;
            prev_value2 = 1;
            for (int x = -search_x; x <= search_x; x++)
            {
                if ((tail_right_x + x > 1) && (tail_right_x + x < image_width))
                {
                    axis_y = image_height;
                    int ymax = tail_axis_y + (image_height / 30);
                    if (ymax >= image_height) ymax = image_height - 1;
                    for (int y = ymax; y > min_y; y--)
                    {
                        int n = (y * Stride) + ((tail_right_x + x) * 3);
                        int n2 = (y * Stride) + ((tail_right_x + x - 2) * 3);
                        int n3 = ((y + 2) * Stride) + ((tail_right_x + x) * 3);
                        if ((horizontal_profile[y] > 2) && (foreground[n] == 0) && (foreground[n3] == 0) && (foreground[n2] == 0) && ((prev_value == 0) || (prev_value2 == 0)))
                        {
                            axis_y = y;
                        }
                        prev_value2 = prev_value;
                        prev_value = foreground[n];
                    }
                    if (axis_y < tail_right_y) tail_right_y = axis_y;
                }
            }
        }

        /// <summary>
        /// locate points on the tail from manually selected ones
        /// </summary>
        private void locateKeypointsManual()
        {
            tailPoint p1 = null, p2 = null;
            tail_left_x = image_width;
            tail_right_x = 0;
            tail_axis_x = 0;
            tail_axis_y = 0;

            if (manual_points.Count == 5)
            {
                int y = 0;                

                for (int i = 0; i < manual_points.Count; i++)
                {
                    tailPoint pt = (tailPoint)manual_points[i];
                    if (pt.y > y)
                    {
                        y = pt.y;
                        p1 = pt;
                    }
                }
                y = 0;
                for (int i = 0; i < manual_points.Count; i++)
                {
                    tailPoint pt = (tailPoint)manual_points[i];
                    if ((pt.y > y) && (pt != p1))
                    {
                        y = pt.y;
                        p2 = pt;
                    }
                }

                if (p1.x < p2.x)
                {
                    tail_base_left_x = p1.x;
                    tail_base_left_y = p1.y;
                    tail_base_right_x = p2.x;
                    tail_base_right_y = p2.y;
                }
                else
                {
                    tail_base_left_x = p2.x;
                    tail_base_left_y = p2.y;
                    tail_base_right_x = p1.x;
                    tail_base_right_y = p1.y;
                }
            }


            if ((manual_points.Count == 3) || (manual_points.Count == 5))
            {
                for (int i = 0; i < manual_points.Count; i++)
                {
                    tailPoint pt = (tailPoint)manual_points[i];
                    if ((pt != p1) && (pt != p2))
                    {
                        if (pt.x < tail_left_x)
                        {
                            tail_left_x = pt.x;
                            tail_left_y = pt.y;
                        }
                        if (pt.x > tail_right_x)
                        {
                            tail_right_x = pt.x;
                            tail_right_y = pt.y;
                        }
                        if (pt.y > tail_axis_y)
                        {
                            tail_axis_x = pt.x;
                            tail_axis_y = pt.y;
                        }
                    }
                }
                tail_width = tail_right_x - tail_left_x;
            }

        }


        /// <summary>
        /// locate the centre point of the tail
        /// </summary>
        private void locateCentralAxis()
        {
            if (manual_points.Count > 2)
            {
                locateKeypointsManual();
            }
            else
            {
                int min_y = image_height / 5;
                int min_x = -1, max_x = 0;
                getTailWidth(ref min_x, ref max_x, 0);
                //if (min_x < 0) getTailWidth(ref min_x, ref max_x, 0);

                tail_width = (max_x - min_x);
                int centre_x = min_x + (tail_width / 2);

                // find the horizontal centre of the tail
                int neighbourhood = tail_width / 30;
                int min_vertical = 9999;
                int min_vertical_x = centre_x;
                for (int x = centre_x - neighbourhood; x < centre_x + neighbourhood; x++)
                {
                    if (vertical_profile[x] < min_vertical)
                    {
                        min_vertical = vertical_profile[x];
                        min_vertical_x = x;
                    }
                }
                tail_axis_x = min_vertical_x;

                int prev_value = 1;
                int prev_value2 = 1;
                int axis_y = 0;
                tail_axis_y = image_height;

                int search_x = neighbourhood / 2;
                for (int x = -search_x; x <= search_x; x++)
                {
                    if ((tail_axis_x + x > -1) && (tail_axis_x + x < image_width))
                    {
                        axis_y = image_height;
                        for (int y = image_height - 4; y > min_y; y--)
                        {
                            int n = (y * Stride) + ((tail_axis_x + x) * 3);
                            int n2 = ((y + 3) * Stride) + ((tail_axis_x + x) * 3);
                            if ((foreground[n] == 0) && (foreground[n2] == 0) && ((prev_value == 0) || (prev_value2 == 0)))
                            {
                                axis_y = y;
                            }
                            prev_value2 = prev_value;
                            prev_value = foreground[n];
                        }
                        if (axis_y < tail_axis_y) tail_axis_y = axis_y;
                    }
                }
            }
            //showKeypoint(tail_axis_x, tail_axis_y, image_width / 150, 0, 255, 0);
        }

        private void completeEdge(ArrayList edge)
        {
            ArrayList new_edge = new ArrayList();
            tailPoint pt = null, prev_pt = null;

            for (int i = 0; i < edge.Count; i++)
            {
                pt = (tailPoint)edge[i];
                if (i > 0)
                {
                    int dx = pt.x - prev_pt.x;
                    int dy = pt.y - prev_pt.y;
                    if ((dx > 1) || (dy > 1) || (dx < -1) || (dy < -1))
                    {
                        int incr = 1;
                        if (Math.Abs(dx) > Math.Abs(dy))
                        {
                            if (prev_pt.x > pt.x) incr = -1;
                            int x = prev_pt.x;
                            while (x != pt.x + incr)
                            {
                                int y = prev_pt.y + (Math.Abs(x - prev_pt.x) * dy / Math.Abs(dx));
                                tailPoint new_pt = new tailPoint(x, y);
                                new_edge.Add(new_pt);
                                x += incr;
                            }
                        }
                        else
                        {
                            if (prev_pt.y > pt.y) incr = -1;
                            int y = prev_pt.y;
                            while (y != pt.y + incr)
                            {
                                int x = prev_pt.x + (Math.Abs(y - prev_pt.y) * dx / Math.Abs(dy));
                                tailPoint new_pt = new tailPoint(x, y);
                                new_edge.Add(new_pt);
                                y += incr;
                            }
                        }
                    }
                    else new_edge.Add(pt);
                }
                else new_edge.Add(pt);
                prev_pt = pt;
            }

            edge.Clear();
            for (int i = 0; i < new_edge.Count; i++)
            {
                tailPoint pt2 = (tailPoint)new_edge[i];
                edge.Add(pt2);
            }
        }


        /// <summary>
        /// return the total pixel intensity within the given region
        /// </summary>
        /// <param name="img"></param>
        /// <param name="img_width"></param>
        /// <param name="img_height"></param>
        /// <param name="tx"></param>
        /// <param name="ty"></param>
        /// <param name="bx"></param>
        /// <param name="by"></param>
        /// <returns></returns>
        private int getIntensityWithinArea(Byte[] img, int img_width, int img_height,
                                           int tx, int ty, int bx, int by)
        {
            int intensity = 0;
            //int hits = 0;
            for (int x = tx; x < bx; x++)
            {
                for (int y = ty; y < by; y++)
                {
                    int n = ((y * img_width) + x) * 3;
                    //if (img[n] != 255)
                    {
                        intensity += img[n + 2] + img[n + 1] + img[n];
                        //hits+=3;
                    }
                }
            }
            //if (hits > 0) intensity /= hits;
            return (intensity);
        }

        /// <summary>
        /// returns the maximal orientation within a local radius of the given point
        /// This is used to detect the orientation of features used for recognition
        /// </summary>
        /// <param name="img"></param>
        /// <param name="img_width"></param>
        /// <param name="img_height"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="radius"></param>
        /// <param name="orientation"></param>
        private void getOrientationWithinArea(Byte[] img, int img_width, int img_height,
                                              int x, int y, int radius,
                                              ref int orientation)
        {
            int no_of_angles = 40;
            int max = 0;
            orientation = 0;
            for (int ang = 0; ang < no_of_angles; ang++)
            {
                float angle = (float)(ang * 2 * Math.PI / no_of_angles);
                int tot = 0;
                for (int r = 1; r < radius; r++)
                {
                    int xx = x + (int)(r * Math.Sin(angle));
                    if ((xx > -1) && (xx < img_width))
                    {
                        int yy = y + (int)(r * Math.Cos(angle));
                        if ((yy > -1) && (yy < img_height))
                        {
                            int n = ((yy * img_width) + xx) * 3;
                            tot += (img[n] * img[n]);
                        }
                    }
                }
                if (tot > max)
                {
                    max = tot;
                    orientation = ang * 360 / no_of_angles;
                }
            }
        }

        private void backgroundSubtract(int blue_threshold, int green_threshold, int intensity_threshold)
        {
            int free_area = 0;
            int max_free_area = 0;

            if (raw_image != null)
            {
                int pixels = image_width * image_height;

                foreground = new Byte[pixels * 3];
                horizontal_profile = new int[image_height];
                vertical_profile = new int[image_width];
                vertical_profile_smoothed = new int[image_width];
                horizontal_profile_max = 0;
                vertical_profile_max = 0;

                keypoints = new Byte[pixels * 3];
                for (int i = 0; i < pixels * 3; i++) keypoints[i] = raw_image[i];

                if (image_width > image_height)
                {
                    profiles = new Byte[image_width * 101 * 3];
                    for (int i = 0; i < image_width * 101 * 3; i++) profiles[i] = (Byte)255;
                }
                else
                {
                    profiles = new Byte[image_height * 101 * 3];
                    for (int i = 0; i < image_height * 101 * 3; i++) profiles[i] = (Byte)255;
                }

                foreground_tx = image_width;
                foreground_ty = image_height;
                foreground_bx = 0;
                foreground_by = 0;
                bool found = false;
                int last_found = 0;

                // remove blue
                for (int y = 0; y < image_height; y++)
                {
                    int hits = 0;
                    int blue_hits = 0;
                    for (int x = 0; x < image_width; x++)
                    {
                        free_area++;

                        int i = ((image_width * y) + x) * 3;

                        bool isBackground = false;

                        // is this a blue pixel ?
                        int blue = (raw_image[i] * 2) - (raw_image[i + 1] + raw_image[i + 2]);
                        if (blue > blue_threshold)
                        {
                            isBackground = true;
                            blue_hits++;
                        }

                        // is this a green pixel ?
                        int green = (raw_image[i+1] * 2) - (raw_image[i] + raw_image[i + 2]);
                        if (green > green_threshold)
                        {
                            isBackground = true;
                        }

                        // is the intensity below a threshold ?
                        int intensity = (raw_image[i] + raw_image[i + 1] + raw_image[i + 2]);
                        if (intensity > intensity_threshold) isBackground = true;

                        // take note of the bounding box for the foreground
                        if (!isBackground)
                        {
                            if (x < foreground_tx) foreground_tx = x;
                            if (y < foreground_ty) foreground_ty = y;
                            if (x > foreground_bx) foreground_bx = x;
                            if (y > foreground_by) foreground_by = y;
                            hits++;
                        }

                        // update the foreground image
                        for (int col = 0; col < 3; col++)
                        {
                            if ((isBackground) || (last_found > 5))
                                foreground[i + col] = 255;
                            else
                            {
                                // record the max area of uninterrupted space
                                if (free_area > max_free_area)
                                    max_free_area = free_area;
                                free_area = 0;

                                foreground[i + col] = 0; // raw_image[i + col];
                                vertical_profile[x]++;
                                if (vertical_profile[x] > vertical_profile_max) vertical_profile_max = vertical_profile[x];
                                horizontal_profile[y]++;
                                if (horizontal_profile[y] > horizontal_profile_max) horizontal_profile_max = horizontal_profile[y];
                            }
                        }
                    }

                    if ((found) && (hits == 0) && (blue_hits > image_width / 10))
                    {
                        last_found = 9999;
                    }

                    if ((found) && (last_found < 5))
                    {
                        if (hits > 50)
                            last_found = 0;
                        else
                            last_found++;
                    }

                    if ((!found) && (hits > 50))
                    {
                        found = true;
                    }
                }
                if (free_area > max_free_area)
                    max_free_area = free_area;
            }

            // remove possible waves
            max_free_area = max_free_area * 100 / (image_width * image_height);
            if (max_free_area < 20)
            {
                waveSuppression();
            }

            //normalise vertical and horizontal profiles
            if (vertical_profile_max > 0)
            {
                for (int x = 0; x < image_width; x++)
                    if (vertical_profile[x] > 0)
                        vertical_profile[x] = vertical_profile[x] * 100 / vertical_profile_max;

                int w = image_width / 40;
                if (w < 1) w = 1;
                for (int x = w; x < image_width-w; x++)
                    if (vertical_profile[x] > 15)
                    {
                        int av = 0;
                        int vhits = 0;
                        for (int xx = x - w; xx < x + w; xx++)
                        {
                            av += vertical_profile[xx];
                            vhits++;
                        }
                        vertical_profile_smoothed[x] = av * 100 / (vertical_profile_max * vhits);
                    }


                for (int x = 0; x < image_width; x++)
                {
                    if (vertical_profile_smoothed[x] > 0)
                    {
                        int n = ((100 - vertical_profile_smoothed[x]) * Stride) + (x * 3);
                        if (n > -1)
                        {
                            profiles[n] = 0;
                            profiles[n + 1] = 0;
                            profiles[n + 2] = (Byte)255;
                        }
                    }
                }
            }
            if (horizontal_profile_max > 0)
                for (int y = 0; y < image_height-1; y++)
                {
                    if (horizontal_profile[y] > 0)
                    {
                        horizontal_profile[y] = horizontal_profile[y] * 100 / horizontal_profile_max;
                        int n = ((100 - horizontal_profile[y]) * Stride) + (y * Stride / image_height);
                        if (n < profiles.Length)
                        {
                            profiles[n] = 0;
                            profiles[n + 1] = (Byte)255;
                            profiles[n + 2] = 0;
                        }
                    }
                }
        }

        private void locateTailSide(int start_x, int start_y, bool isLeft, ArrayList side)
        {
            int x = start_x;
            float x2 = start_x;
            int y = start_y;
            int leftval, rightval;

            int leeway = tail_width / 10;
            int search_width = tail_width / 40;
            int patch_width = leeway / 2;
            if (patch_width < 2) patch_width = 2;
            int n = 0, n2 = 0, n3 = 0;
            int winner_x = 1;

            side.Clear();

            // clipped on the left
            if (start_x < image_width / 20)
            {
                bool finished = false;
                while ((!finished) && (y < image_height))
                {
                    finished = true;
                    n = (y * Stride) + (1 * 3);
                    n2 = (y * Stride) + (4 * 3);
                    n3 = (y * Stride) + (6 * 3);
                    if ((foreground[n] == 0) || (foreground[n2] == 0) || (foreground[n3] == 0))
                    {
                        winner_x = 1;
                        side.Add(new tailPoint(winner_x, y));
                        x2 = (x2 * 0.7f) + (winner_x * 0.3f);
                        x = (int)x2;
                        showKeypoint(keypoints, image_width, image_height, x, y, line_width, 255, 255, 0);
                        finished = false;
                        y++;
                    }
                }
            }

            // clipped on the right
            if (start_x > image_width - (image_width / 20))
            {
                bool finished = false;
                while ((!finished) && (y < image_height))
                {
                    finished = true;
                    n = (y * Stride) + ((image_width - 2) * 3);
                    n2 = (y * Stride) + ((image_width - 4) * 3);
                    n3 = (y * Stride) + ((image_width - 6) * 3);
                    if ((foreground[n] == 0) || (foreground[n2] == 0) || (foreground[n3] == 0))
                    {
                        winner_x = image_width - 1;
                        side.Add(new tailPoint(winner_x, y));
                        x2 = (x2 * 0.7f) + (winner_x * 0.3f);
                        x = (int)x2;
                        showKeypoint(keypoints, image_width, image_height, x, y, line_width, 255, 255, 0);
                        finished = false;
                        y++;
                    }
                }
            }


            int prev_x = x;
            int prev_x2 = x;
            int prev_x3 = x;
            while (y < foreground_by)
            {
                int diff = 0;
                int prev_diff = 0;
                int max_diff = 0;
                winner_x = x;
                for (int xx = x - search_width; xx < x + search_width; xx++)
                {
                    leftval = 0;
                    rightval = 0;
                    for (int xxx = xx - patch_width; xxx < xx + patch_width; xxx++)
                    {
                        if ((xxx > -1) && (xxx < image_width - 1))
                        {
                            n = (y * Stride) + (xxx * 3);
                            //n2 = ((y * image_width) + xxx + 1) * 3;
                            n2 = ((y + 1) * Stride) + ((xxx + 1) * 3);

                            int blueval1 = (raw_image[n] * 2) - raw_image[n + 1] - raw_image[n + 2];
                            if (blueval1 < 0) blueval1 = 0;
                            int blueval2 = (raw_image[n2] * 2) - raw_image[n2 + 1] - raw_image[n2 + 2];
                            if (blueval2 < 0) blueval2 = 0;

                            //int rightblue = (raw_image[n2] * 2) - raw_image[n2 + 1] - raw_image[n2 + 2];

                            if (xxx < xx)
                                leftval += raw_image[n] + raw_image[n2] + blueval1 + blueval2;
                            else
                                rightval += raw_image[n] + raw_image[n2] + blueval1 + blueval2;
                        }
                    }

                    if (isLeft)
                        diff = leftval - rightval;
                    else
                        diff = rightval - leftval;

                    int diff2 = diff + prev_diff;
                    prev_diff = diff;

                    if (diff2 > max_diff)
                    {
                        max_diff = diff2;
                        winner_x = xx;
                    }
                }
                if ((isLeft) && (winner_x < x - leeway)) winner_x = x - leeway;
                if ((isLeft) && (winner_x < start_x - (leeway / 2))) winner_x = start_x - (leeway / 2);
                if ((!isLeft) && (winner_x > x + leeway)) winner_x = x + leeway;
                if ((!isLeft) && (winner_x > start_x + (leeway / 2))) winner_x = start_x + (leeway / 2);

                bool located = false;
                if ((horizontal_profile[y] > 20) || (y < tail_axis_y))
                {
                    x2 = (x2 * 0.7f) + (winner_x * 0.3f);
                    x = (int)x2;
                    located = true;
                }

                if (y > tail_axis_y)
                {
                    if ((isLeft) && (prev_x > x + 1) && (prev_x2 > prev_x + 1) && (prev_x3 > prev_x2 + 1)) y = foreground_by;
                    if ((!isLeft) && (prev_x < x - 1) && (prev_x2 < prev_x - 1) && (prev_x3 < prev_x2 - 1)) y = foreground_by;
                }

                if ((y < foreground_by) && (located))
                {
                    showKeypoint(keypoints, image_width, image_height, x, y, line_width, 255, 255, 0);
                    side.Add(new tailPoint(x, y));
                }

                prev_x3 = prev_x2;
                prev_x2 = prev_x;
                prev_x = x;
                y++;
            }

            if (side.Count > 3)
            {
                tailPoint pt = (tailPoint)side[side.Count - 1];
                tailPoint pt2 = (tailPoint)side[side.Count - 2];
                tailPoint pt3 = (tailPoint)side[side.Count - 3];
                tailPoint pt4 = (tailPoint)side[side.Count - 4];
                side.Remove(pt);
                side.Remove(pt2);
                side.Remove(pt3);
                side.Remove(pt4);
            }
        }


        private void locateTailSideManual(int start_x, int start_y, int end_x, int end_y, bool isLeft, ArrayList side)
        {
            int x = start_x;
            float x2 = start_x;
            int y = start_y;
            int leftval, rightval;

            int leeway = tail_width / 10;
            int search_width = tail_width / 40;
            int patch_width = leeway / 2;
            if (patch_width < 2) patch_width = 2;
            int n = 0, n2 = 0;
            int winner_x = 1;

            side.Clear();


            int prev_x = x;
            int prev_x2 = x;
            int prev_x3 = x;
            while (y < end_y)
            {
                int diff = 0;
                int prev_diff = 0;
                int max_diff = 0;
                float fraction = (y - start_y) / (float)(end_y - start_y);
                x = start_x + (int)((end_x - start_x) * fraction);

                int adjust = (int)(tail_width / 8 * ((Math.Cos(fraction * Math.PI) + 1) / 2.0f));
                if (isLeft)
                    x -= adjust;
                else
                    x += adjust;

                if (x < tail_left_x-10) x = tail_left_x-10;
                if (x > tail_right_x+10) x = tail_right_x+10;

                winner_x = x;
                for (int xx = x - search_width; xx < x + search_width; xx++)
                {
                    leftval = 0;
                    rightval = 0;
                    for (int xxx = xx - patch_width; xxx < xx + patch_width; xxx++)
                    {
                        if ((xxx > -1) && (xxx < image_width - 1))
                        {
                            n = (y * Stride) + (xxx * 3);
                            //n2 = ((y * image_width) + xxx + 1) * 3;
                            n2 = ((y + 1) * Stride) + ((xxx + 1) * 3);

                            int blueval1 = (raw_image[n] * 2) - raw_image[n + 1] - raw_image[n + 2];
                            if (blueval1 < 0) blueval1 = 0;
                            int blueval2 = (raw_image[n2] * 2) - raw_image[n2 + 1] - raw_image[n2 + 2];
                            if (blueval2 < 0) blueval2 = 0;

                            //int rightblue = (raw_image[n2] * 2) - raw_image[n2 + 1] - raw_image[n2 + 2];

                            if (xxx < xx)
                                leftval += raw_image[n] + raw_image[n2] + blueval1 + blueval2;
                            else
                                rightval += raw_image[n] + raw_image[n2] + blueval1 + blueval2;
                        }
                    }

                    if (isLeft)
                        diff = leftval - rightval;
                    else
                        diff = rightval - leftval;

                    int diff2 = diff + prev_diff;
                    prev_diff = diff;

                    if (diff2 > max_diff)
                    {
                        max_diff = diff2;
                        winner_x = xx;
                    }
                }
                if ((isLeft) && (winner_x < x - leeway)) winner_x = x - leeway;
                if ((isLeft) && (winner_x < start_x - (leeway / 2))) winner_x = start_x - (leeway / 2);
                if ((!isLeft) && (winner_x > x + leeway)) winner_x = x + leeway;
                if ((!isLeft) && (winner_x > start_x + (leeway / 2))) winner_x = start_x + (leeway / 2);

                bool located = false;
                if ((horizontal_profile[y] > 20) || (y < tail_axis_y))
                {
                    x2 = (x2 * 0.7f) + (winner_x * 0.3f);
                    x = (int)x2;
                    located = true;
                }

                if (y > tail_axis_y)
                {
                    if ((isLeft) && (prev_x > x + 1) && (prev_x2 > prev_x + 1) && (prev_x3 > prev_x2 + 1)) y = foreground_by;
                    if ((!isLeft) && (prev_x < x - 1) && (prev_x2 < prev_x - 1) && (prev_x3 < prev_x2 - 1)) y = foreground_by;
                }

                if ((y < foreground_by) && (located))
                {
                    showKeypoint(keypoints, image_width, image_height, x, y, line_width, 255, 255, 0);

                    if (x < 0) x = 0;
                    if (x > image_width - 1) x = image_width - 1;
                    if (y < 0) y = 0;
                    if (y > image_height - 1) y = image_height - 1;

                    side.Add(new tailPoint(x, y));
                }

                prev_x3 = prev_x2;
                prev_x2 = prev_x;
                prev_x = x;
                y++;
            }

        }


        /// <summary>
        /// remove low scoring features, leaving only the strongest surviving
        /// </summary>
        private void nonmax_supress(ArrayList features, int max_features)
        {
            ArrayList victims = new ArrayList();

            for (int i = 0; i < features.Count; i++)
            {
                tailPoint pt = (tailPoint)features[i];
                for (int j = 0; j < features.Count; j++)
                {
                    tailPoint pt2 = (tailPoint)features[j];
                    if (pt2 != pt)
                    {
                        if (pt2.intensity < pt.intensity)
                        {
                            int dx = pt2.x - pt.x;
                            if ((dx > -5) && (dx < 5))
                            {
                                int dy = pt2.y - pt.y;
                                if ((dy > -5) && (dy < 5))
                                    victims.Add(pt2);
                            }
                        }
                    }
                }
            }
            for (int i = 0; i < victims.Count; i++)
                features.Remove(victims[i]);

            while (features.Count > max_features)
            {
                int lowest = 0;
                int victim = -1;
                for (int i = 0; i < features.Count; i++)
                {
                    tailPoint pt = (tailPoint)features[i];
                    if ((i == 0) || (pt.intensity < lowest))
                    {
                        lowest = pt.intensity;
                        victim = i;
                    }
                }
                if (victim > -1)
                    features.RemoveAt(victim);
            }
        }


        private void locateKeyFeatures()
        {
            key_features.Clear();

            // calculate the spot radius
            int spot_radius = tail_image_width / 100;
            if (spot_radius < 1) spot_radius = 1;
            int spot_outer_radius = spot_radius * 2;
            int centre_pixels = spot_radius * spot_radius * 4 * 3;
            int surround_pixels = (spot_outer_radius * spot_outer_radius * 4 * 3) - centre_pixels;
            int border = spot_outer_radius * 1;

            int thresh = 30;

            int max_height = tail_image_height * 80 / 100;
            int prev_spot_x = -1;
            int prev_spot_y = -1;
            for (int x = spot_outer_radius; x < tail_image_width - spot_outer_radius; x++)
            {
                for (int y = border; y < max_height - (border * 2); y++)
                {
                    int n_above = (((y - border) * tail_image_width) + x) * 3;
                    if (!((tail_image[n_above] == tail_image_background_b) && (tail_image[n_above + 1] == tail_image_background_g)))
                    {
                        int n_below = (((y + (border * 2)) * tail_image_width) + x) * 3;
                        if (!((tail_image[n_below] == tail_image_background_b) && (tail_image[n_below + 1] == tail_image_background_g)))
                        {
                            int n = ((y * tail_image_width) + x) * 3;
                            int n2 = ((y * tail_image_width) + x - spot_outer_radius) * 3;
                            int n3 = (((y + spot_radius) * tail_image_width) + x) * 3;
                            int n4 = (((y + spot_radius) * tail_image_width) + x - spot_outer_radius) * 3;
                            if ((edges_image[n2] == 0) && (edges_image[n] == 255) &&
                                (edges_image[n4] == 0) && (edges_image[n3] == 255))
                            {
                                int centre = getIntensityWithinArea(edges_image, tail_image_width, tail_image_height, x - spot_radius, y - spot_radius, x + spot_radius, y + spot_radius);
                                int av_centre = centre / centre_pixels;
                                if (av_centre > thresh)
                                {
                                    int surround = getIntensityWithinArea(edges_image, tail_image_width, tail_image_height, x - spot_outer_radius, y - spot_outer_radius, x + spot_outer_radius, y + spot_outer_radius);
                                    surround -= centre;
                                    int av_surround = surround / surround_pixels;
                                    if (av_surround < av_centre / 2)
                                    {
                                        int n_left = ((y * tail_image_width) + x - spot_outer_radius) * 3;
                                        if (!((tail_image[n_left] == tail_image_background_b) && (tail_image[n_left + 1] == tail_image_background_g)))
                                        {
                                            int n_right = ((y * tail_image_width) + x + spot_outer_radius) * 3;
                                            if (!((tail_image[n_right] == tail_image_background_b) && (tail_image[n_right + 1] == tail_image_background_g)))
                                            {
                                                // get the position of the spot in standard tail coordinates
                                                int sx = tail_image_coords[x, y, 0];
                                                int sy = tail_image_coords[x, y, 1];

                                                // avoid the trailing edge of the centre of the tail
                                                // which is often subject to segmentation errors
                                                if (!((sx > -8) && (sx < 8) && (sy > 70)))
                                                {
                                                    int dist = Math.Abs(x - prev_spot_x) + Math.Abs(y - prev_spot_y);
                                                    if (dist > spot_outer_radius)
                                                    {
                                                        // look up the centre surround in the standardised tail image
                                                        // to determine whether this is on-surround or off-surround
                                                        centre = getIntensityWithinArea(tail_image, tail_image_width, tail_image_height, x - spot_radius, y - spot_radius, x + spot_radius, y + spot_radius);
                                                        av_centre = centre / centre_pixels;
                                                        surround = getIntensityWithinArea(tail_image, tail_image_width, tail_image_height, x - spot_outer_radius, y - spot_outer_radius, x + spot_outer_radius, y + spot_outer_radius);
                                                        surround -= centre;
                                                        av_surround = surround / surround_pixels;
                                                        int feature_type = 0;
                                                        if (av_centre > av_surround + 10) feature_type = 1;  // off-surround
                                                        if (av_surround > av_centre + 10) feature_type = 2;  // on-surround

                                                        // get the local orientation
                                                        int orientation = 0;
                                                        getOrientationWithinArea(edges_image, tail_image_width, tail_image_height, x, y, spot_outer_radius, ref orientation);

                                                        int feature_intensity = av_centre - av_surround;
                                                        if (feature_intensity < 0) feature_intensity = -feature_intensity;

                                                        tailPoint feature = new tailPoint(sx, sy, feature_type, orientation, feature_intensity);
                                                        key_features.Add(feature);

                                                        //showKeypoint(tail_image, tail_image_width, tail_image_height, x, y, spot_radius, 0, 255, 0);
                                                        //prev_spot_x = x;
                                                        //prev_spot_y = y;
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// updates properties of key features which govern relative spatial distribution
        /// </summary>
        private void updateGeometry(ArrayList features)
        {
            // update the histograms for each feature
            for (int i = 0; i < features.Count - 1; i++)
            {
                tailPoint pt = (tailPoint)features[i];
                for (int j = i + 1; j < features.Count; j++)
                {
                    if (i != j)
                    {
                        tailPoint pt2 = (tailPoint)features[j];
                        int dx = pt2.x - pt.x;
                        int dy = pt2.y - pt.y;
                        int dist = (int)Math.Sqrt((dx*dx)+(dy*dy));
                        pt.updateDensityHistogram(pt2.Type, dist);
                        pt2.updateDensityHistogram(pt.Type, dist);
                    }
                }
            }
        }

        /// <summary>
        /// find LinearFeatures on the tail, as connected sets of features
        /// </summary>
        private void locateLinearFeatures()
        {
            LinearFeatures.Clear();
            LinearFeaturesAll.Clear();

            int longest = 0;
            int radius_x = 6;
            int radius_y = 20;
            int max_orient_diff = 40;
            for (int f = 0; f < key_features.Count; f++)
            {
                tailPoint feature = (tailPoint)key_features[f];
                int Type = feature.Type;
                for (int f2 = 0; f2 < key_features.Count; f2++)
                {
                    if (f != f2)
                    {
                        tailPoint feature2 = (tailPoint)key_features[f2];
                        if ((Type == feature2.Type) || (feature2.Type == 0))
                        {
                            int dx = feature2.x - feature.x;
                            if (Math.Abs(dx) < radius_x)
                            {
                                int dy = feature2.y - feature.y;
                                if (Math.Abs(dy) < radius_y)
                                {
                                    int dorient = feature2.Orientation - feature.Orientation;
                                    if (dorient < 0) dorient = -dorient;
                                    if (dorient > 180) dorient = 360 - dorient;
                                    if (dorient < max_orient_diff)
                                    {
                                        tailPoint pt1 = feature;
                                        tailPoint pt2 = feature2;
                                        if (dy > 0)
                                        {
                                            pt1 = feature2;
                                            pt2 = feature;
                                        }
                                        if (pt1.connections == null)
                                            pt1.connections = new ArrayList();
                                        pt1.connections.Add(pt2);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            ArrayList path = new ArrayList();
            for (int f = 0; f < key_features.Count; f++)
            {
                tailPoint feature = (tailPoint)key_features[f];
                tailPoint pt = feature;
                tailPoint pt_new;
                path.Clear();
                path.Add(pt);

                while (pt != null)
                {
                    pt_new = null;
                    if (pt.connections != null)
                    {
                        float min_dist = 0;
                        for (int f2 = 0; f2 < pt.connections.Count; f2++)
                        {
                            tailPoint feature2 = (tailPoint)pt.connections[f2];
                            if (feature2.y < pt.y)
                            {
                                int dxx = Math.Abs(feature.x - pt.x);
                                if (Math.Abs(dxx) < 3)
                                {
                                    int dy = Math.Abs(feature2.y - pt.y);
                                    int dx = Math.Abs(feature2.x - pt.x);
                                    float dist = (float)Math.Sqrt((dx * dx) + (dy * dy));
                                    if ((f2 == 0) || (dist < min_dist))
                                    {
                                        min_dist = dist;
                                        pt_new = feature2;
                                    }
                                }
                            }
                        }
                    }
                    else pt = null;

                    pt = pt_new;
                    if (pt != null)
                    {
                        tailPoint path_pt = pt.Copy();
                        LinearFeaturesAll.Add(path_pt);
                        path.Add(path_pt);
                    }
                }
                if (path.Count > 3)
                {
                    if (path.Count > longest) longest = path.Count;

                    LinearFeatures.Add(path);

                    showScratch(path, tail_features, tail_image_width, tail_image_height);

                    path = new ArrayList();
                }
            }
        }


        private void locateTailEdgeDisscontinuities(bool leftSide, int min_magnitude, int max_edges)
        {
            tailPoint prev_pt = null;
            ArrayList disscontinuities = new ArrayList();
            ArrayList tail_edge = tail_left;
            if (!leftSide) tail_edge = tail_right;

            int min_x = tail_left_x;
            int max_x = tail_right_x;

            // calculate the total length of the edge
            float edge_length = 0;
            int direction = 0;
            int prev_direction = 0;
            float magnitude = 0;
            int same_direction = 0;
            int start_x = 0;
            int end_x = 0;
            for (int i = 0; i < tail_edge.Count; i++)
            {
                tailPoint pt = (tailPoint)tail_edge[i];

                if (i > 0)
                {
                    if ((pt.x > min_x) && (pt.x < max_x))
                    {
                        int dx = pt.x - prev_pt.x;
                        int dy = pt.y - prev_pt.y;
                        float dist = (float)Math.Sqrt((dx * dx) + (dy * dy));
                        if (dy > 0) direction = 1;
                        if (dy < 0) direction = 0;
                        magnitude += dist;
                        if (prev_direction != direction)
                        {
                            //if ((magnitude > min_magnitude) && (direction == 1) && (same_direction > 1))
                            if ((magnitude > 2) && (direction == 1) && (same_direction > min_magnitude))
                            {
                                tailPoint edge_pt = new tailPoint(prev_pt.x, prev_pt.y);
                                edge_pt.offset = (int)edge_length;
                                edge_pt.intensity = same_direction;
                                disscontinuities.Add(edge_pt);
                            }
                            magnitude = 0;
                            same_direction = 0;
                        }
                        else same_direction++;
                        edge_length += dist;
                        prev_direction = direction;
                    }
                }
                else start_x = pt.x;
                end_x = pt.x;
                prev_pt = pt;
            }
            if (edge_length > 0)
            {
                // remove points close to the centre of the tail                
                int dist1 = end_x - start_x;
                if (dist1 < 0) dist1 = -dist1;
                if (dist1 > 0)
                {
                    int min_offset = (int)(dist1 / 5);
                    for (int i = disscontinuities.Count - 1; i >= 0; i--)
                    {
                        tailPoint pt = (tailPoint)disscontinuities[i];
                        int dist2 = pt.x - start_x;
                        if (dist2 < 0) dist2 = -dist2;
                        if (dist2 < min_offset)
                            disscontinuities.Remove(pt);
                    }


                    // sort by magnitude
                    for (int i = 0; i < disscontinuities.Count - 1; i++)
                    {
                        tailPoint pt1 = (tailPoint)disscontinuities[i];
                        int winner = -1;
                        for (int j = i + 1; j < disscontinuities.Count; j++)
                        {
                            tailPoint pt2 = (tailPoint)disscontinuities[j];
                            if (pt2.y > pt1.y)
                            {
                                winner = j;
                            }
                        }
                        if (winner > -1)
                        {
                            disscontinuities[i] = (tailPoint)disscontinuities[winner];
                            disscontinuities[winner] = pt1;
                        }
                    }

                    for (int i = disscontinuities.Count - 1; i >= max_edges; i--)
                        disscontinuities.RemoveAt(i);

                    for (int i = 0; i < disscontinuities.Count; i++)
                    {
                        tailPoint pt = (tailPoint)disscontinuities[i];
                        int diff = pt.x - start_x;
                        if (diff < 0) diff = -diff;
                        int index = (int)(diff * (EDGE_PATTERN_BINS/2) / dist1);
                        if (!leftSide)
                            index += (EDGE_PATTERN_BINS/2);
                        else
                            index = (EDGE_PATTERN_BINS/2) - index;
                        if (index >= EDGE_PATTERN_BINS) index = EDGE_PATTERN_BINS - 1;
                        edge_pattern[index] = true;
                        util.drawLine(keypoints, image_width, image_height, pt.x, pt.y, pt.x, pt.y - 20, 0, 0, 255, 2, false);
                    }
                }
            }
        }

        private void updateSillouette()
        {
            completeEdge(tail_left);
            completeEdge(tail_right);
            completeEdge(tail_left_side);
            completeEdge(tail_right_side);

            sillouette = new bool[image_width, image_height, 3];

            foreground_tx = image_width - 1;
            foreground_ty = image_height - 1;
            foreground_bx = 0;
            foreground_by = 0;

            for (int i = 0; i < tail_left.Count; i++)
            {
                tailPoint pt = (tailPoint)tail_left[i];
                sillouette[pt.x, pt.y, 0] = true;
                if (pt.x < foreground_tx) foreground_tx = pt.x;
                if (pt.x > foreground_bx) foreground_bx = pt.x;
                if (pt.y < foreground_ty) foreground_ty = pt.y;
                if (pt.y > foreground_by) foreground_by = pt.y;
            }
            for (int i = 0; i < tail_right.Count; i++)
            {
                tailPoint pt = (tailPoint)tail_right[i];
                sillouette[pt.x, pt.y, 0] = true;
                if (pt.x < foreground_tx) foreground_tx = pt.x;
                if (pt.x > foreground_bx) foreground_bx = pt.x;
                if (pt.y < foreground_ty) foreground_ty = pt.y;
                if (pt.y > foreground_by) foreground_by = pt.y;
            }
            tailPoint l_pt = null, r_pt = null;
            for (int i = 0; i < tail_left_side.Count; i++)
            {
                l_pt = (tailPoint)tail_left_side[i];
                sillouette[l_pt.x, l_pt.y, 1] = true;
                if (l_pt.x < foreground_tx) foreground_tx = l_pt.x;
                if (l_pt.x > foreground_bx) foreground_bx = l_pt.x;
                if (l_pt.y < foreground_ty) foreground_ty = l_pt.y;
                if (l_pt.y > foreground_by) foreground_by = l_pt.y;
            }
            for (int i = 0; i < tail_right_side.Count; i++)
            {
                r_pt = (tailPoint)tail_right_side[i];
                sillouette[r_pt.x, r_pt.y, 1] = true;
                if (r_pt.x < foreground_tx) foreground_tx = r_pt.x;
                if (r_pt.x > foreground_bx) foreground_bx = r_pt.x;
                if (r_pt.y < foreground_ty) foreground_ty = r_pt.y;
                if (r_pt.y > foreground_by) foreground_by = r_pt.y;
            }

            // draw the waterline
            if ((l_pt != null) && (r_pt != null))
            {
                int dx = r_pt.x - l_pt.x;
                int dy = r_pt.y - l_pt.y;
                for (int x = l_pt.x; x <= r_pt.x; x++)
                {
                    int y = l_pt.y + ((x - l_pt.x) * dy / dx);
                    sillouette[x, y, 1] = true;
                    sillouette[x, y, 2] = true;
                    showKeypoint(keypoints, image_width, image_height, x, y, line_width, 255, 255, 0);
                }
            }

            for (int x = foreground_tx; x <= foreground_bx; x++)
            {
                bool fill = false;
                for (int y = foreground_ty; y <= foreground_by; y++)
                {
                    if (sillouette[x, y, 0]) fill = true;
                    if (sillouette[x, y, 1])
                    {
                        fill = false;
                        sillouette[x, y, 0] = true;
                    }
                    if (fill)
                    {
                        sillouette[x, y, 0] = true;
                        //showKeypoint(keypoints, x, y, 0, 255, 255, 0);
                    }
                }
            }

            for (int y = foreground_ty; y <= foreground_by; y++)
            {
                int prev_x = -1;
                for (int x = tail_axis_x + (tail_width / 10); x <= foreground_bx; x++)
                {
                    if ((sillouette[x, y, 0]) && (prev_x != -1))
                    {
                        for (int xx = prev_x; xx < x; xx++)
                        {
                            sillouette[xx, y, 0] = true;
                            //showKeypoint(keypoints, xx, y, 0, 255, 255, 0);
                        }
                        prev_x = -1;
                    }
                    if ((sillouette[x - 1, y, 0]) && (!sillouette[x, y, 0])) prev_x = x;
                }
                prev_x = -1;
                for (int x = tail_axis_x - (tail_width / 10); x >= foreground_tx; x--)
                {
                    if ((sillouette[x, y, 0]) && (prev_x != -1))
                    {
                        for (int xx = prev_x; xx > x; xx--)
                        {
                            sillouette[xx, y, 0] = true;
                            //showKeypoint(keypoints, xx, y, 0, 255, 255, 0);
                        }
                        prev_x = -1;
                    }
                    if ((sillouette[x + 1, y, 0]) && (!sillouette[x, y, 0])) prev_x = x;
                }
            }
        }


        /// <summary>
        /// locate key points on the tail
        /// </summary>
        private bool locateKeypoints()
        {
            bool success = false;

            total_inconsistency = -1;
            tail_roll = 0;
            tail_tilt = 0;
            tail_pan = 0;

            locateCentralAxis();
            if (tail_axis_y < image_height)
            {
                if (manual_points.Count == 0) locateTailTips();
                if ((tail_left_x > -1) && (tail_right_x > -1) &&
                    (tail_left_y < image_height) && (tail_right_y < image_height))
                {
                    traceEdge(tail_axis_x, tail_axis_y, tail_left_x, tail_left_y, tail_left);
                    traceEdge(tail_axis_x, tail_axis_y, tail_right_x, tail_right_y, tail_right);


                    if (tail_left.Count > 0)
                    {
                        int xx = ((tailPoint)tail_left[tail_left.Count - 1]).x;
                        int yy = ((tailPoint)tail_left[tail_left.Count - 1]).y;
                        if (yy < tail_left_y - (image_height / 30))
                        {
                            tail_left_x = xx;
                            tail_left_y = yy;
                        }
                    }
                    if (tail_right.Count > 0)
                    {
                        int xx = ((tailPoint)tail_right[tail_right.Count - 1]).x;
                        int yy = ((tailPoint)tail_right[tail_right.Count - 1]).y;
                        if (yy < tail_right_y - (image_height / 30))
                        {
                            tail_right_x = xx;
                            tail_right_y = yy;
                        }
                    }

                    if (manual_points.Count != 5)
                    {
                        locateTailSide(tail_left_x, tail_left_y, true, tail_left_side);
                        locateTailSide(tail_right_x, tail_right_y, false, tail_right_side);
                    }
                    else
                    {
                        locateTailSideManual(tail_left_x, tail_left_y, tail_base_left_x, tail_base_left_y, true, tail_left_side);
                        locateTailSideManual(tail_right_x, tail_right_y, tail_base_right_x, tail_base_right_y, false, tail_right_side);
                    }

                    total_inconsistency = (edgeInconsistency(tail_left) +
                                                edgeInconsistency(tail_right) +
                                                edgeInconsistency(tail_left_side) +
                                                edgeInconsistency(tail_right_side)) / 4;

                    tailPoint left_side_end = null;
                    tailPoint right_side_end = null;
                    if (tail_left_side.Count > 0)
                        left_side_end = (tailPoint)tail_left_side[tail_left_side.Count - 1];
                    if (tail_right_side.Count > 0)
                        right_side_end = (tailPoint)tail_right_side[tail_right_side.Count - 1];

                    if ((left_side_end != null) && (right_side_end != null))
                    {
                        if ((left_side_end.x > tail_left_x) && (right_side_end.x < tail_right_x))
                        {
                            if (total_inconsistency < inconsistency_threshold)
                            {
                                success = true;
                            }
                        }
                    }

                }
            }
            return (success);
        }


        /// <summary>
        /// look for any anomolies in the edge data and correct them
        /// </summary>
        /// <param name="start_x"></param>
        /// <param name="start_y"></param>
        /// <param name="end_x"></param>
        /// <param name="end_y"></param>
        /// <param name="edge"></param>
        private int validateEdge(int start_x, int start_y, int end_x, int end_y, ArrayList edge, int divisor, bool forceDelete)
        {
            int coverage = 0;
            ArrayList victims = new ArrayList();
            float average_y = 0;
            int max_y_diff = image_height / divisor;

            int max = edge.Count / 10;
            if (max < 20) max = 20;
            for (int i = 0; i < edge.Count; i++)
            {
                tailPoint pt = (tailPoint)edge[i];

                if ((i > max) && (((pt.y > average_y + max_y_diff) || (pt.y < average_y - max_y_diff))))
                {
                    victims.Add(pt);
                }
                else
                {
                    if (average_y == 0)
                        average_y = pt.y;
                    else
                        average_y = (average_y * 0.7f) + (pt.y * 0.3f);
                }
            }

            if (edge.Count > 0)
                coverage = (edge.Count - victims.Count) * 100 / edge.Count;

            // remove any victims
            if ((coverage >= 50) || (forceDelete))
            {
                for (int i = 0; i < victims.Count; i++)
                {
                    tailPoint pt = (tailPoint)victims[i];
                    edge.Remove(pt);
                }
            }

            // adjust the centre point if necessary
            for (int i = 0; i < edge.Count; i++)
            {
                tailPoint pt = (tailPoint)edge[i];
                if (pt.y > tail_axis_y)
                {
                    int dxx = tail_axis_x - pt.x;
                    if (dxx < 0) dxx = -dxx;
                    if (dxx < tail_width / 10)
                    {
                        tail_axis_x = pt.x;
                        tail_axis_y = pt.y;
                    }
                }
            }
            return (coverage);
        }

        /// <summary>
        /// trace along the trailing edge of the tail
        /// </summary>
        /// <param name="start_x"></param>
        /// <param name="start_y"></param>
        /// <param name="end_x"></param>
        /// <param name="end_y"></param>
        /// <param name="edge"></param>
        private void traceEdge(int start_x, int start_y, int end_x, int end_y, ArrayList edge)
        {
            //clear the edge
            edge.Clear();

            int incr = 1;
            if (end_x < start_x) incr = -1;
            int x = start_x;
            int x_length = end_x - start_x;
            int y_length = end_y - start_y;
            int tot_dist = (int)Math.Sqrt((x_length * x_length) + (y_length * y_length));
            int half_dist = tot_dist / 2;
            int scan_y = image_height / 40;
            int patch_y = 5;
            if (x_length < 0) x_length = -x_length;

            while (x != end_x)
            {
                int dx = x - start_x;
                if (dx < 0) dx = -dx;
                int y = start_y + (dx * (end_y - start_y) / x_length);
                int dy = y - start_y;

                int dist = (int)Math.Sqrt((dx * dx) + (dy * dy));
                scan_y = 20 + ((image_height / 40) * (half_dist - (int)Math.Abs(dist - half_dist)) / half_dist);

                // scan vertically to locate the sharpest edge
                int edge_y = 0;
                int max_diff = 0;
                for (int yy = y - (scan_y * 3); yy < y + scan_y; yy++)
                {
                    if ((yy > patch_y) && (yy < image_height - patch_y))
                    {
                        int above = 0;
                        int below = 0;
                        int n, n2 = 0, n3 = 0;

                        for (int yyy = yy - patch_y; yyy < yy + patch_y; yyy++)
                        {
                            n = (yyy * Stride) + (x * 3);
                            if (x + 1 < image_width)
                                n2 = (yyy * Stride) + ((x + 1) * 3);
                            if (x - 1 > 0)
                                n3 = (yyy * Stride) + ((x - 1) * 3);

                            /*
                            int value = ((raw_image[n] + raw_image[n2]) * 2) -
                                     ((raw_image[n + 2] + raw_image[n2 + 2]) + (raw_image[n + 1] + raw_image[n2 + 1]));
                            if (value < 0) value = 0;
                            int value2 = ((raw_image[n+2] + raw_image[n2+2]) * 2) -
                                     ((raw_image[n] + raw_image[n2]) + (raw_image[n + 1] + raw_image[n2 + 1]));
                            if (value2 < 0) value = 0;
                             */

                            int value = raw_image[n] + raw_image[n2] + raw_image[n3];

                            if (yyy < yy)
                            {
                                above += value;
                            }
                            else
                            {
                                below += value;
                            }
                        }
                        int diff = above - below;
                        if (diff > max_diff)
                        {
                            max_diff = diff;
                            edge_y = yy;
                        }
                    }
                }

                if (max_diff > 0)
                {
                    tailPoint pt = new tailPoint(x, edge_y);
                    edge.Add(pt);
                }

                x += incr;
            }

            // remove any invalid points
            int coverage = validateEdge(start_x, start_y, end_x, end_y, edge, 40, false);
            if (coverage < 50) coverage = validateEdge(start_x, start_y, end_x, end_y, edge, 10, true);

            // show the edge
            for (int i = 0; i < edge.Count; i++)
            {
                tailPoint pt = (tailPoint)edge[i];
                showKeypoint(keypoints, image_width, image_height, pt.x, pt.y, line_width, 255, 255, 0);
            }
        }


        /// <summary>
        /// get the width of the tail in pixels
        /// </summary>
        /// <param name="min_x"></param>
        /// <param name="max_x"></param>
        /// <param name="min_vertical_value"></param>
        private void getTailWidth(ref int min_x, ref int max_x, int min_vertical_value)
        {
            min_x = -1;
            max_x = 0;
            for (int x = 1; x < image_width - 1; x++)
            {
                //if ((vertical_profile_smoothed[x - 1] > min_vertical_value) && (vertical_profile_smoothed[x] > min_vertical_value) && (vertical_profile_smoothed[x + 1] > min_vertical_value))
                if (vertical_profile_smoothed[x] > min_vertical_value)
                {
                    max_x = x;
                    if (min_x == -1)
                        min_x = x;
                }
            }
        }

        /// <summary>
        /// return a figure indicating the amount of consistency in the edge data
        /// very inconsistent edges are likely to be due to incorrect segmentation
        /// </summary>
        /// <param name="edge"></param>
        /// <returns></returns>
        private float edgeInconsistency(ArrayList edge)
        {
            tailPoint pt, prev_pt = null;
            float inconsistency = 0;

            for (int i = 0; i < edge.Count; i++)
            {
                pt = (tailPoint)edge[i];
                if (i > 0)
                {
                    int dx = pt.x - prev_pt.x;
                    if (dx < 0) dx = -dx;
                    int dy = pt.y - prev_pt.y;
                    if (dy < 0) dy = -dy;
                    inconsistency += dx + dy;
                }
                prev_pt = pt;
            }
            if (edge.Count > 0) inconsistency /= edge.Count;
            return (inconsistency);
        }


        /// <summary>
        /// try to determine the orientation of the tail relative to the camera
        /// </summary>
        private void locateTailOrientation()
        {
            // calculate the roll angle
            /*
            int dx = tail_left_x - tail_right_x;
            int dy = tail_left_y - tail_right_y;
            dy = dy * image_width / image_height;
            int tail_tips_dist = (int)Math.Sqrt((dx * dx) + (dy * dy));
            tail_roll = (float)Math.Asin(dx / tail_tips_dist);
             * */

            // calculate the "V" angle of the tail to the camera
            int dx_left_side = tail_axis_x - tail_left_x;
            int dy_left_side = tail_axis_y - tail_left_y;
            dy_left_side = dy_left_side * image_width / image_height;
            int dx_right_side = tail_right_x - tail_axis_x;
            int dy_right_side = tail_right_y - tail_axis_y;
            dy_right_side = dy_right_side * image_width / image_height;
            float left_side_dist = (float)Math.Sqrt((dx_left_side * dx_left_side) + (dy_left_side * dy_left_side));
            float right_side_dist = (float)Math.Sqrt((dx_right_side * dx_right_side) + (dy_right_side * dy_right_side));
            float left_rotation = (float)Math.Asin(dy_left_side / left_side_dist);
            if (dx_left_side < 0) left_rotation = (float)(Math.PI * 2) - left_rotation;
            float right_rotation = (float)Math.Asin(dy_right_side / right_side_dist);
            if (dx_right_side < 0) right_rotation = (float)(Math.PI * 2) - right_rotation;
            float V_angle = right_rotation - left_rotation;
            if (V_angle < 0) V_angle = (2 * (float)Math.PI) + V_angle;
            if (V_angle > (float)Math.PI) V_angle = (2 * (float)Math.PI) - V_angle;
            float dist = (left_side_dist + right_side_dist) / 2;
            V_angle = (float)Math.PI - V_angle;
            float V_angle2 = V_angle;
            float V_angle_degrees = (V_angle * 180 / (float)Math.PI);
            if (V_angle_degrees < normal_V_angle)
            {
                tail_pan = (normal_V_angle - V_angle_degrees) * 90 / normal_V_angle;
                if (left_side_dist < right_side_dist) tail_pan = -tail_pan;
                V_angle_degrees = normal_V_angle;
                V_angle = (V_angle_degrees + 0.001f) * (float)Math.PI / 180.0f;
            }
            else
            {
                tail_pan = (left_side_dist - right_side_dist) * 90 / (left_side_dist + right_side_dist);
            }
            tail_pan = tail_pan * (float)Math.PI / 180.0f;
            float vertical_displacement = dist * (float)Math.Sin(((float)Math.PI - V_angle) / 2);

            float full_vertical_angle = (180 - normal_V_angle) * (float)Math.PI / 180.0f;
            float full_vertical_displacement = dist * (float)Math.Sin(full_vertical_angle / 2);

            // calculate the tilt angle, by comparing the vertical difference
            // between the centre of the tail and the tips
            tail_tilt = (float)Math.Asin(vertical_displacement / full_vertical_displacement);
            float tail_tilt_degrees = (tail_tilt * 180 / (float)Math.PI);

            // locate the base of the tail

            float ang1 = -(float)(Math.PI / 2) - right_rotation;
            float ang2 = (float)(Math.PI / 2) - left_rotation;
            tail_roll = ang1 + ((ang2 - ang1) / 2);
            //tail_roll = -(float)(Math.PI / 2) + (((float)Math.PI - V_angle2) / 2) + (V_angle2 / 2); // -(float)(Math.PI / 2);
            tail_base_x = (int)(vertical_displacement * 1.2f * (float)Math.Sin(tail_roll));
            tail_base_y = (int)(vertical_displacement * 1.2f * (float)Math.Cos(tail_roll));
            tail_base_y = tail_base_y * image_height / image_width;
            tail_base_x += tail_axis_x;
            tail_base_y += tail_axis_y;
            //showKeypoint(keypoints, tail_base_x, tail_base_y, 10, 0, 0, 255);
            //showLine(tail_base_x, tail_base_y, tail_axis_x, tail_axis_y, 255, 255, 255, false);
        }

        /// <summary>
        /// suppress features which are a single pixel in height
        /// </summary>
        private void waveSuppression()
        {
            for (int x = 0; x < image_width; x++)
            {
                for (int y = 1; y < image_height - 1; y++)
                {
                    int n = (y * Stride) + (x * 3);
                    if (foreground[n] == 0)
                    {
                        int n_above = ((y - 1) * Stride) + (x * 3);
                        if (foreground[n_above] > 0)
                        {
                            int n_below = ((y + 1) * Stride) + (x * 3);
                            if (foreground[n_below] > 0)
                            {
                                for (int col = 0; col < 3; col++)
                                    foreground[n + col] = (Byte)255;
                            }
                        }
                    }
                }
            }
        }

        #endregion

        #region "display functions"

        /// <summary>
        /// show a map for the given species
        /// </summary>
        /// <param name="species"></param>
        private void showMapBySpecies(String species, String individual)
        {
            float ang = (float)Math.PI / 20;

            if (map_image != null)
            {
                float tx = util.MapToFloat(txtTopLongitude.Text);
                float ty = util.MapToFloat(txtTopLattitude.Text);
                float bx = util.MapToFloat(txtBottomLongitude.Text);
                float by = util.MapToFloat(txtBottomLattitude.Text);

                if (tx >= bx) tx = -360 + tx;

                if ((bx > tx) && (by > ty))
                {
                    // copy the original map
                    Byte[] map_image_species = new Byte[map_image_width * map_image_height * 3];
                    picMap.Image = new Bitmap(map_image_width, map_image_height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                    for (int i = 0; i < map_image_width * map_image_height * 3; i++)
                        map_image_species[i] = map_image[i];

                    // plot sightings on the map
                    ArrayList sightings = database.getSightingsBySpecies(species, individual);
                    for (int i = 0; i < sightings.Count; i++)
                    {
                        sighting s = (sighting)sightings[i];
                        float heading = s.heading;

                        if ((s.longitude >= tx) && (s.longitude <= bx) &&
                            (s.lattitude >= ty) && (s.lattitude <= by))
                        {
                            int x = (int)((s.longitude - tx) * map_image_width / (bx - tx));
                            int y = (int)((s.lattitude - ty) * map_image_height / (by - ty));

                            // draw the whale icon
                            float w = map_image_width * whale_icon_size;
                            float h = w * map_image_height / map_image_width;
                            int linewidth = 1;

                            if (heading >= 0)
                            {
                                // V shape indicates heading
                                float angle = heading * (float)Math.PI / 180.0f;
                                angle = (2 * (float)Math.PI) - angle;
                                int xdisp = (int)((w / 2) * Math.Sin(angle));
                                int ydisp = (int)((h / 2) * Math.Cos(angle));

                                int x1 = x - xdisp + (int)(w * Math.Sin(angle - ang));
                                int y1 = y - ydisp + (int)(h * Math.Cos(angle - ang));
                                int x2 = x - xdisp + (int)(w * Math.Sin(angle + ang));
                                int y2 = y - ydisp + (int)(h * Math.Cos(angle + ang));
                                
                                util.drawLine(map_image_species, map_image_width, map_image_height,
                                              x, y, x1, y1, whale_icon_r, whale_icon_g, whale_icon_b, linewidth, false);
                                util.drawLine(map_image_species, map_image_width, map_image_height,
                                              x, y, x2, y2, whale_icon_r, whale_icon_g, whale_icon_b, linewidth, false);
                            }
                            else
                            {
                                w /= 5;
                                h /= 5;
                                // X marks the spot
                                util.drawLine(map_image_species, map_image_width, map_image_height,
                                              x - (int)w, y - (int)h, x + (int)w, y + (int)h, whale_icon_r, whale_icon_g, whale_icon_b, linewidth, false);
                                util.drawLine(map_image_species, map_image_width, map_image_height,
                                              x + (int)w, y - (int)h, x - (int)w, y + (int)h, whale_icon_r, whale_icon_g, whale_icon_b, linewidth, false);
                            }
                        }
                    }

                    // show the map
                    BitmapArrayConversions.updatebitmap_unsafe(map_image_species, (Bitmap)(picMap.Image));
                    picMap.Refresh();
                }
            }
        }

        /// <summary>
        /// show a key point on the tail
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="radius"></param>
        /// <param name="r"></param>
        /// <param name="g"></param>
        /// <param name="b"></param>
        private void showKeypoint(Byte[] img, int img_width, int img_height, int x, int y, int radius, int r, int g, int b)
        {
            for (int xx = x - radius; xx <= x + radius; xx++)
            {
                if ((xx > -1) && (xx < img_width))
                {
                    for (int yy = y - radius; yy <= y + radius; yy++)
                    {
                        if ((yy > -1) && (yy < img_height))
                        {
                            int n = ((yy * img_width) + xx) * 3;
                            img[n] = (Byte)b;
                            img[n + 1] = (Byte)g;
                            img[n + 2] = (Byte)r;
                        }
                    }
                }
            }
        }




        /// <summary>
        /// show the key features which are used for recognition
        /// </summary>
        /// <param name="img"></param>
        /// <param name="img_width"></param>
        /// <param name="img_height"></param>
        private void showKeyFeatures(Byte[] img, int img_width, int img_height)
        {
            int radius = img_width / 50;
            for (int f = 0; f < key_features.Count; f++)
            {
                tailPoint feature = (tailPoint)key_features[f];

                if (feature.Orientation != -1)// && (feature.intensity > 10))
                {
                    int std_img_x = 0, std_img_y = 0;
                    getTailImageCoordinate(feature.x, feature.y, ref std_img_x, ref std_img_y);

                    int r = 0;
                    int g = 0;
                    int b = 255;
                    if (feature.Type == 1)
                    {
                        r = 255;
                        g = 0;
                        b = 0;
                    }
                    if (feature.Type == 2)
                    {
                        r = 0;
                        g = 255;
                        b = 0;
                    }

                    showCircle(img, img_width, img_height,
                               std_img_x, std_img_y, radius, 1, feature.Orientation, r, g, b);
                }
            }

        }



        /// <summary>
        /// draw a key feature within the given standardised tail image
        /// </summary>
        /// <param name="img"></param>
        /// <param name="img_width"></param>
        /// <param name="img_height"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="type"></param>
        /// <param name="orientation"></param>
        /// <param name="radius"></param>
        private void showKeyFeature(Byte[] img, int img_width, int img_height,
                                    int x, int y, int type, int orientation, int radius)
        {
            float angle = orientation / 180.0f * (float)Math.PI;
            for (int r = 0; r < radius; r++)
            {
                int xx = x + (int)(r * Math.Sin(angle));
                int yy = y + (int)(r * Math.Cos(angle));
                if ((xx > -1) && (xx < img_width) && (yy > -1) && (yy < img_height))
                {
                    int rr = 0;
                    int gg = 0;
                    int bb = 255;
                    if (type == 1)
                    {
                        rr = 255;
                        gg = 0;
                        bb = 0;
                    }
                    if (type == 2)
                    {
                        rr = 0;
                        gg = 255;
                        bb = 0;
                    }

                    showKeypoint(img, img_width, img_height, xx, yy, 1, rr, gg, bb);
                    /*
                    int n = ((yy * img_width) + xx) * 3;
                    img[n] = (Byte)bb;
                    img[n+1] = (Byte)gg;
                    img[n+2] = (Byte)rr;
                     */
                }
            }
        }




        /// <summary>
        /// draw a line
        /// </summary>
        /// <param name="start_x"></param>
        /// <param name="start_y"></param>
        /// <param name="end_x"></param>
        /// <param name="end_y"></param>
        /// <param name="r"></param>
        /// <param name="g"></param>
        /// <param name="b"></param>
        private void showLine(int start_x, int start_y, int end_x, int end_y, int r, int g, int b, bool visibleOnly)
        {
            showLine(keypoints, image_width, image_height, start_x, start_y, end_x, end_y, r, g, b, visibleOnly);
        }

        /// <summary>
        /// show a scratch in the given image
        /// </summary>
        /// <param name="scratch"></param>
        /// <param name="img"></param>
        /// <param name="img_width"></param>
        /// <param name="img_height"></param>
        private void showScratch(ArrayList scratch, Byte[] img, int img_width, int img_height)
        {
            int prev_x=0;
            int prev_y=0;
            for (int i = 0; i < scratch.Count; i++)
            {
                tailPoint pt = (tailPoint)scratch[i];
                int std_img_x = 0;
                int std_img_y = 0;
                getTailImageCoordinate(pt.x,pt.y,ref std_img_x,ref std_img_y);
                if (i > 0)
                {
                    showLine(img, img_width, img_height, std_img_x, std_img_y, prev_x, prev_y, 255,0,0,false);
                }
                prev_x = std_img_x;
                prev_y = std_img_y;
            }
        }

        /// <summary>
        /// show a line in an image
        /// </summary>
        /// <param name="img"></param>
        /// <param name="img_width"></param>
        /// <param name="img_height"></param>
        /// <param name="start_x"></param>
        /// <param name="start_y"></param>
        /// <param name="end_x"></param>
        /// <param name="end_y"></param>
        /// <param name="r"></param>
        /// <param name="g"></param>
        /// <param name="b"></param>
        /// <param name="visibleOnly"></param>
        private void showLine(Byte[] img, int img_width, int img_height,
                              int start_x, int start_y, int end_x, int end_y, int r, int g, int b, bool visibleOnly)
        {
            int dx = end_x - start_x;
            int dy = end_y - start_y;

            int incr = 1;
            if (Math.Abs(dx) > Math.Abs(dy))
            {
                if (dx < 0) incr = -1;
                int x = start_x;
                while ((x != end_x + incr) && (x > 1) && (x < img_width - 2))
                {
                    int y = start_y + ((x - start_x) * dy / dx);
                    if ((y > -1) && (y < img_height))
                    {
                        bool sillouette_active = false;
                        if (visibleOnly)
                            sillouette_active = sillouette[x, y, 0];

                        if ((!visibleOnly) || ((visibleOnly) && (sillouette_active)))
                            showKeypoint(img, img_width, img_height, x, y, line_width, r, g, b);
                    }
                    x += incr;
                }
            }
            else
            {
                if (dy < 0) incr = -1;
                if (dy != 0)
                {
                    int y = start_y;
                    while (y != end_y + incr)
                    {
                        int x = start_x + ((y - start_y) * dx / dy);
                        if ((x > -1) && (x < img_width) && (y < img_height))
                        {
                            bool sillouette_active = false;
                            if (visibleOnly)
                                sillouette_active = sillouette[x, y, 0];

                            if ((!visibleOnly) || ((visibleOnly) && (sillouette_active)))
                                showKeypoint(img, img_width, img_height, x, y, line_width, r, g, b);
                        }
                        y += incr;
                    }
                }
            }
        }


        /// <summary>
        /// show the coordinate grid superimposed upon the original image
        /// </summary>
        private void showTailGrid()
        {
            showLine(tail_base_x, tail_base_y, tail_left_x2, tail_left_y2, 255, 0, 0, true);
            showLine(tail_base_x, tail_base_y, tail_right_x2, tail_right_y2, 255, 0, 0, true);

            int image_x = 0, image_y = 0;
            for (int x = -100; x <= 100; x+= 10)
                getTailCoordinate(x, 0, ref image_x, ref image_y, true);
            //showKeypoint(keypoints, image_x, image_y, 10, 0, 0, 255);
        }

        /// <summary>
        /// show a circle in the given image
        /// </summary>
        /// <param name="img"></param>
        /// <param name="img_width"></param>
        /// <param name="img_height"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="radius"></param>
        /// <param name="line_width"></param>
        /// <param name="orientation"></param>
        /// <param name="r"></param>
        /// <param name="g"></param>
        /// <param name="b"></param>
        private void showCircle(Byte[] img, int img_width, int img_height,
                                int x, int y, int radius, int line_width, int orientation,
                                int r, int g, int b)
        {
            int no_of_angles = 40;
            int orient = orientation * no_of_angles / 360;
            for (int ang = 0; ang < no_of_angles; ang++)
            {
                float angle = (float)(ang * 2 * Math.PI / no_of_angles);
                int start_rr = radius - line_width - 1;
                if (start_rr < 0) start_rr = 0;
                if (ang == orientation) start_rr = 0;
                for (int rr = start_rr; rr < radius; rr++)
                {
                    int xx = x + (int)(rr * Math.Sin(angle));
                    if ((xx > -1) && (xx < img_width))
                    {
                        int yy = y + (int)(rr * Math.Cos(angle));
                        if ((yy > -1) && (yy < img_height))
                        {
                            int n = ((yy * img_width) + xx) * 3;
                            img[n] = (Byte)b;
                            img[n + 1] = (Byte)g;
                            img[n + 2] = (Byte)r;
                        }
                    }
                }
            }
        }

        #endregion

        #region "coordinate transforms"

        /// <summary>
        /// given a standard geometry coordinate return a tail picture pixel location
        /// this is used to produce a standardised picture of a tail
        /// </summary>
        /// <param name="standard_geometry_x"></param>
        /// <param name="standard_geometry_y"></param>
        /// <param name="image_x"></param>
        /// <param name="image_y"></param>
        private void getTailImageCoordinate(float standard_geometry_x, float standard_geometry_y,
                                            ref int image_x, ref int image_y)
        {
            int tail_base_height = tail_image_height * 80 / 100;
            int half_width = tail_image_width / 2;
            int half_height = tail_base_height / 2;

            float sx = standard_geometry_x;
            if (sx < 0) sx = -sx;

            int max_width = tail_base_height * 50 / 100;
            int min_width = tail_base_height * 30 / 100;
            int tip_width = half_width / 10;
            int centre_point = half_width / 30;

            int tx = (int)(half_width * sx / 100);
            int ty = (int)((tail_base_height * 40 / 100) * (100 - sx) / 100);
            int bx = tx;
            int h = min_width + (int)(max_width * (100 - sx) / 100);
            // round off the tips
            int xx = half_width - tip_width;
            float fraction = 0;
            if (tx > xx)
            {
                fraction = 1.0f - (((tx - xx) / (float)tip_width) * 40 / 100);
                h = (int)(h * fraction);
            }
            if (tx < centre_point)
            {
                fraction = 0.1f - ((tx / (float)centre_point) * 100 / 1000);
                ty += (int)(fraction * max_width);
                h -= (int)(fraction * 2 * max_width);
                if (h < 0) h = 0;
            }
            int by = ty + h;

            if (standard_geometry_x > 0)
            {
                tx += half_width;
                bx += half_width;
            }
            else
            {
                tx = half_width - tx;
                bx = half_width - tx;
            }

            image_x = tx;
            image_y = ty + (int)((by - ty) * (100 - standard_geometry_y) / 100);

            if (image_x < 0) image_x = 0;
            if (image_x >= tail_image_width) image_x = tail_image_width - 1;
            if (image_y < 0) image_y = 0;
            if (image_y >= tail_base_height) image_y = -1; // tail_base_height - 1;
        }


        /// <summary>
        /// returns the image coordinate given the standard coordinate
        /// </summary>
        /// <param name="standard_geometry_x"></param>
        /// <param name="standard_geometry_y"></param>
        /// <param name="image_x"></param>
        /// <param name="image_y"></param>
        private void getTailCoordinate(float standard_geometry_x, float standard_geometry_y,
                                       ref int image_x, ref int image_y, bool draw_line)
        {            
            int dx, dy;
            float dist=0,angle=0,x=-1,y=-1, dist2=0,baseline_x,baseline_y;
            int left_length = tail_axis_x - tail_left_x2;
            int right_length = tail_right_x2 - tail_axis_x;

            // calculate the fractional distance along the length of the tail section
            // TODO: adjust this for pan angle
            float fraction_x = standard_geometry_x / 100.0f;
            float fraction_y = standard_geometry_y / 100.0f;
            if (fraction_x < 0) fraction_x = -fraction_x;

            if (standard_geometry_x < 0)
            {
                dx = tail_base_x - tail_left_x2;
                dy = tail_base_y - tail_left_y2;
            }
            else
            {
                dx = tail_right_x2 - tail_base_x;
                dy = tail_right_y2 - tail_base_y;
            }

            dist = (float)Math.Sqrt((dx * dx) + (dy * dy));
            angle = (float)Math.Asin(dx / dist);
            if (dy < 0) angle = (float)(Math.PI * 2) - angle;
            dist2 = fraction_x * dist;
            baseline_x = tail_base_x - (dist2 * (float)Math.Sin(angle));
            baseline_y = tail_base_y - (dist2 * (float)Math.Cos(angle));

            if ((baseline_x > -1) && (baseline_x < image_width) &&
                (baseline_y > -1) && (baseline_y < image_height))
            {
                int d_trailing = 0;
                int d_leading = 0;
                if ((prev_coordinate_x == standard_geometry_x) && (prev_d_trailing > -1))
                {
                    d_trailing = prev_d_trailing;
                    d_leading = prev_d_leading;
                }
                else
                {
                    leading_edge_length_unknown = false;
                    int x_trailing = (int)baseline_x, y_trailing = (int)baseline_y;
                    float extra_angle = (float)Math.PI;
                    while (((sillouette[x_trailing, y_trailing, 0]) || (sillouette[x_trailing, y_trailing, 1]) || (y_trailing >= tail_axis_y)) && (y_trailing > 10) && (y_trailing < image_height - 10))
                    {
                        x_trailing = (int)(d_trailing * (float)Math.Sin(tail_roll + extra_angle));
                        y_trailing = (int)(d_trailing * (float)Math.Cos(tail_roll + extra_angle) * image_height / image_width);
                        x_trailing += (int)baseline_x;
                        y_trailing += (int)baseline_y;

                        if ((x_trailing > -1) && (x_trailing < image_width) &&
                            (y_trailing > -1) && (y_trailing < image_height))
                        {
                            if ((draw_line) && (sillouette[x_trailing, y_trailing, 0]))
                                showKeypoint(keypoints, image_width, image_height, x_trailing, y_trailing, 1, 255, 0, 0);
                        }
                        else
                        {
                            x_trailing = 0;
                            y_trailing = 0;
                            d_trailing = 0;
                        }

                        d_trailing++;
                    }
                    d_trailing--;
                    if (d_trailing < 0) d_trailing = 0;
                    prev_d_trailing = d_trailing;

                    extra_angle = 0;
                    int x_leading = (int)baseline_x, y_leading = (int)baseline_y;
                    while ((sillouette[x_leading, y_leading, 0]) && (y_leading > 10) && (y_leading < image_height - 10))
                    {
                        x_leading = (int)(d_leading * (float)Math.Sin(tail_roll + extra_angle));
                        y_leading = (int)(d_leading * (float)Math.Cos(tail_roll + extra_angle) * image_height / image_width);
                        x_leading += (int)baseline_x;
                        y_leading += (int)baseline_y;

                        if ((x_leading > -1) && (x_leading < image_width) &&
                            (y_leading > -1) && (y_leading < image_height))
                        {
                            if (sillouette[x_leading, y_leading, 2])
                                leading_edge_length_unknown = true;
                            if (draw_line)
                                showKeypoint(keypoints, image_width, image_height, x_leading, y_leading, 1, 255, 0, 0);
                        }
                        else
                        {
                            x_leading = 0;
                            y_leading = 0;
                            d_leading = 0;
                        }

                        d_leading++;
                    }
                    d_leading--;
                    if (d_leading < 0) d_leading = 0;
                    prev_d_leading = d_leading;
                }

                int threshold = (int)(fraction_x * 100);
                float thresh2 = (1.0f - fraction_x);
                threshold = 50;
                thresh2 = 0.5f;
                float multiplier = 1.0f;
                if (thresh2 > 0) multiplier = 1.0f / thresh2;
                if (standard_geometry_y >= threshold)
                {
                    x = (int)(d_trailing * ((fraction_y - thresh2) * multiplier) * (float)Math.Sin(tail_roll + (float)Math.PI));
                    y = (int)(d_trailing * ((fraction_y - thresh2) * multiplier) * (float)Math.Cos(tail_roll + (float)Math.PI) * image_height / image_width);
                    x += (int)baseline_x;
                    y += (int)baseline_y;
                }
                if (standard_geometry_y < threshold)
                {
                    if (leading_edge_length_unknown)
                    {
                        // TODO: improve this estimation
                        d_leading = d_trailing;
                    }
                    x = (int)(d_leading * ((thresh2 - fraction_y) * multiplier) * (float)Math.Sin(tail_roll));
                    y = (int)(d_leading * ((thresh2 - fraction_y) * multiplier) * (float)Math.Cos(tail_roll) * image_height / image_width);
                    x += (int)baseline_x;
                    y += (int)baseline_y;
                }

            }

            if ((x > 0) && (y > 0))
            {
                if ((x >= image_width) || (y >= image_height))
                {
                    x = -1;
                    y = -1;
                }
                else
                {
                    if (!sillouette[(int)x, (int)y, 0])
                    {
                        x = -1;
                        y = -1;
                    }
                }
            }
            prev_coordinate_x = standard_geometry_x;
            image_x = (int)x;
            image_y = (int)y;
        }

        #endregion

        #region "database functions"

        /// <summary>
        /// show some database statistics
        /// </summary>
        private void updateStatistics()
        {
            lblNoOfSightings.Text = Convert.ToString(database.getNumberOfSightings()) + " Sightings";
            lblNoOfIndividuals.Text = Convert.ToString(database.getNumberOfIndividuals()) + " Individuals";
            lblNoOfSpecies.Text = Convert.ToString(database.getNumberOfSpecies()) + " Species";
            lblVersion.Text = "Version " + VERSION;
        }

        /// <summary>
        /// convert a heading value into a string
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private String valueToHeading(float value)
        {
            String heading = "Unknown";

            if (value == 0) heading = "N";
            if (value == 22.5f) heading = "NNE";
            if (value == 45) heading = "NE";
            if (value == 67.5f) heading = "ENE";
            if (value == 90) heading = "E";
            if (value == 112.5f) heading = "ESE";
            if (value == 135) heading = "SE";
            if (value == 157.5f) heading = "SSE";
            if (value == 180) heading = "S";
            if (value == 202.5f) heading = "SSW";
            if (value == 225) heading = "SW";
            if (value == 247.5f) heading = "WSW";
            if (value == 270) heading = "W";
            if (value == 292.5f) heading = "WNW";
            if (value == 315) heading = "NW";
            if (value == 337.5f) heading = "NNW";
            if (value == -1) heading = "Unknown";
            if (value == -2) heading = "Resting";
            if (value == -3) heading = "Milling";

            return (heading);
        }


        /// <summary>
        /// convert a heading string into a value
        /// </summary>
        /// <param name="heading"></param>
        /// <returns></returns>
        private Single headingToValue(String heading)
        {
            float value = 0;

            if (heading != "")
            {
                // convert to upper case
                if ((heading != "Unknown") && (heading != "Resting") && (heading != "Milling"))
                    heading = heading.ToUpper();

                switch (heading)
                {
                    case "N": { value = 0; break; }
                    case "NNE": { value = 22.5f; break; }
                    case "NE": { value = 45; break; }
                    case "ENE": { value = 67.5f; break; }
                    case "E": { value = 90; break; }
                    case "ESE": { value = 112.5f; break; }
                    case "SE": { value = 135; break; }
                    case "SSE": { value = 157.5f; break; }
                    case "S": { value = 180; break; }
                    case "SSW": { value = 202.5f; break; }
                    case "SW": { value = 225; break; }
                    case "WSW": { value = 247.5f; break; }
                    case "W": { value = 270; break; }
                    case "WNW": { value = 292.5f; break; }
                    case "NW": { value = 315; break; }
                    case "NNW": { value = 337.5f; break; }
                    case "Unknown": { value = -1; break; }
                    case "Resting": { value = -2; break; }
                    case "Milling": { value = -3; break; }
                }
            }

            return (value);
        }

        /// <summary>
        /// copy the image into the sightings directory if necessary
        /// </summary>
        private void copyToSightings()
        {
            if (!ImageFilename.Contains(base_path + "sightings"))
            {
                String DateStr = Convert.ToString(SightingDate.Value.Day) + Convert.ToString(SightingDate.Value.Month) + Convert.ToString(SightingDate.Value.Year) + cmbHour.Text + cmbMin.Text;
                String new_ImageFilename = base_path + "sightings\\" + txtName.Text + DateStr + cmbObserver.Text + ".jpg";
                if (File.Exists(new_ImageFilename)) File.Delete(new_ImageFilename);
                File.Copy(ImageFilename, new_ImageFilename);
                ImageFilename = new_ImageFilename;
            }
        }

        /// <summary>
        /// returns an object containing the current sighting data
        /// </summary>
        /// <returns></returns>
        public whale createWhaleObject()
        {
            float headingValue = headingToValue(cmbHeading.Text);
            String maturity = cmbMaturity.Text;
            int YearBorn = 0;
            if (txtYearBorn.Text != "") YearBorn = Convert.ToInt32(txtYearBorn.Text);            
            whale new_sighting = new whale(cmbObserver.Text, txtName.Text, txtSerialNumber.Text,
                                           cmbSex.Text, YearBorn, maturity, cmbSpecies.Text,
                                           tail_index, Texture, key_features, LinearFeatures, edge_pattern,
                                           txtLattitude.Text,
                                           txtLongitude.Text,
                                           headingValue,
                                           ImageFilename, tailFilename, useForIdentifications, txtNotes.Text,
                                           manual_points,
                                           SightingDate.Value.Day, SightingDate.Value.Month, SightingDate.Value.Year, Convert.ToInt32(cmbHour.Text), Convert.ToInt32(cmbMin.Text));
             
            return (new_sighting);
        }

        private void SearchDatabase()
        {
            bool previously_stored = false;
            
            findwhale = new whale("", "", "", cmbSex.Text, 0, "", cmbSpecies.Text,
                                        tail_index, Texture, key_features,
                                        LinearFeatures, edge_pattern, "", "", 0, ImageFilename, "", false, "",
                                        manual_points,
                                        0,0,0,0,0);
            
            search_indexes = new ArrayList();
            search_results = database.Find(findwhale, search_indexes, compareEdgePattern);
            if (search_results.Count == 1)
            {
                whale found = (whale)search_results[0];                
                sighting s = (sighting)found.Sightings[0];
                String filename = s.ImageFilename;
                if (filename == ImageFilename)
                {
                    if (s.manual_points != null)
                    {
                        if (s.manual_points.Count > 0)
                        {
                            manual_keypoints = true;
                            manual_points = s.manual_points;
                            loadTail(ImageFilename);
                            showManualPoints();
                        }
                    }

                    cmbMapIndividualName.Text = found.Name;
                    showMapBySpecies(found.Species, cmbMapIndividualName.Text);

                    showValuesFromIndividual(found);
                    previously_stored = true;
                }
            }

            // show search results
            grpCompare.Visible = false;
            if ((search_results.Count > 0) && (!previously_stored))
            {
                grpSearchResults.Visible = true;
                showSearchResults();
            }
            else
            {
                grpSearchResults.Visible = false;
            }
        }

        private void showSearchResults()
        {
            PictureBox pic=null;
            TextBox txtbox = null;

            for (int i = 0; i < 4; i++)
            {
                switch (i)
                {
                    case 0: { pic = picSearchResult0; txtbox = txtSearchResult0; break; }
                    case 1: { pic = picSearchResult1; txtbox = txtSearchResult1; break; }
                    case 2: { pic = picSearchResult2; txtbox = txtSearchResult2; break; }
                    case 3: { pic = picSearchResult3; txtbox = txtSearchResult3; break; }
                }

                if (i < search_results.Count)
                {
                    pic.Visible = true;
                    whale w = (whale)search_results[i];
                    sighting s = (sighting)w.Sightings[0];
                    if (File.Exists(s.TailFilename))
                    {
                        pic.Load(s.TailFilename);
                        if (w.SerialNumber != "")
                            txtbox.Text = w.Name + " / " + w.SerialNumber;
                        else
                            txtbox.Text = w.Name;
                    }
                    else
                    {
                        pic.Visible = false;
                        txtbox.Text = "";
                    }
                }
                else
                {
                    pic.Visible = false;
                    txtbox.Text = "";
                }
            }
        }

        private void showValuesFromIndividual(whale found)
        {
            sighting s = (sighting)found.Sightings[0];
            String filename = s.ImageFilename;
            if (filename == ImageFilename)
            {
                cmbObserver.Text = s.Observer;
                txtName.Text = found.Name;
                txtSerialNumber.Text = found.SerialNumber;
                cmbSpecies.Text = found.Species;
                txtLattitude.Text = util.FloatToMap(s.lattitude, false);
                txtLongitude.Text = util.FloatToMap(s.longitude, true);
                cmbHeading.Text = valueToHeading(s.heading);
                cmbSex.Text = found.Sex;
                txtYearBorn.Text = Convert.ToString(found.YearBorn);
                cmbMaturity.Text = found.Maturity;
                txtNotes.Text = s.Notes;
            }
        }

        private void AddToDatabase()
        {
            if (cmbObserver.Text == "")
            {
                MessageBox.Show("Please enter a the observer name");
                cmbObserver.Focus();
            }
            else
            {
                if (txtName.Text == "")
                {
                    MessageBox.Show("Please enter a name for this whale");
                    txtName.Focus();
                }
                else
                {
                    //if (txtSerialNumber.Text == "")
                    //{
                    //    MessageBox.Show("Please enter a serial number");
                    //    txtSerialNumber.Focus();
                    //}
                    //else
                    {
                        if (cmbSpecies.Text == "")
                        {
                            MessageBox.Show("Please enter a species");
                            cmbSpecies.Focus();
                        }
                        else
                        {
                            if ((txtLattitude.Text == "") || (!util.isMapCoordinate(txtLattitude.Text)))
                            {
                                MessageBox.Show("Please enter a lattitude, ending with N or S");
                                txtLattitude.Focus();
                            }
                            else
                            {
                                if ((txtLongitude.Text == "") || (!util.isMapCoordinate(txtLongitude.Text)))
                                {
                                    MessageBox.Show("Please enter a longitude, ending with E or W");
                                    txtLongitude.Focus();
                                }
                                else
                                {
                                    if (cmbHeading.Text == "")
                                    {
                                        MessageBox.Show("Please enter a heading");
                                        cmbHeading.Focus();
                                    }
                                    else
                                    {
                                        if (cmbMaturity.Text == "")
                                        {
                                            MessageBox.Show("Please enter an estimated maturity");
                                            cmbMaturity.Focus();
                                        }
                                        else
                                        {
                                            // copy image into the sightings directory
                                            copyToSightings();

                                            String[] parts = ImageFilename.Split('\\');
                                            String[] parts2 = parts[parts.Length - 1].Split('.');
                                            tailFilename = tailPath + parts2[0] + "_tail.jpg";
                                            //tailFilename = tailPath + "tail" + Convert.ToString(database.no_of_individuals) + ".jpg";

                                            if (File.Exists(tailFilename)) File.Delete(tailFilename);
                                            picTailStandard.Image.Save(tailFilename, System.Drawing.Imaging.ImageFormat.Jpeg);

                                            whale w = createWhaleObject();
                                            int status = database.Add(w);

                                            createIndividualNames();
                                            createIndividualNamesSearch();
                                            cmbMapIndividualName.Text = txtName.Text;
                                            showMapBySpecies(cmbMapSpecies.Text, cmbMapIndividualName.Text);
                                            updateStatistics();
                                            database.Save(database_filename);

                                            if (status == 1)
                                            {
                                                MessageBox.Show("Saved to database");
                                            }
                                            if (status == -2) MessageBox.Show("The database is full");
                                            if (status == 2) MessageBox.Show("Details for this sighting were updated");
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

        }

        #endregion

        #region "main"

        private void update()
        {
            grpSighting.Visible = true;

            selected_search_result_index = -1;
            segmentation_ok = false;
            bool success = false;
            int int_threshold = max_intensity_threshold;
            int best_intensity = -1;
            float min_inconsistency = -1;
            prev_d_leading = -1;
            prev_d_trailing = -1;
            prev_coordinate_x = -1;
            edge_pattern = new bool[EDGE_PATTERN_BINS];

            while ((int_threshold > min_intensity_threshold) && (!success))
            {
                // subtract the background
                backgroundSubtract(blue_threshold, green_threshold, int_threshold);

                // locate key points on the tail
                success = locateKeypoints();

                if (!success)
                {
                    if ((total_inconsistency < min_inconsistency) || (min_inconsistency < 0))
                    {
                        min_inconsistency = total_inconsistency;
                        best_intensity = int_threshold;
                    }
                }

                int_threshold -= 10;
            }

            if ((!success) && (best_intensity > -1))
            {
                // subtract the background
                backgroundSubtract(blue_threshold, green_threshold, best_intensity);

                // locate key points on the tail
                locateKeypoints();
            }

            if (success)
            {
                // estimate the orientation of the tail
                locateTailOrientation();

                // create a sillouette
                updateSillouette();

                // if the tail is tilted at too shallow an angle relative to the camera
                // don't bother looking for edge patterns, since these may not be
                // clearly visible
                compareEdgePattern = false;
                int tail_tilt_degrees = (int)(tail_tilt / Math.PI * 180);
                if (tail_tilt_degrees > 30)
                {
                    locateTailEdgeDisscontinuities(true, 5, 10);
                    locateTailEdgeDisscontinuities(false, 5, 10);
                    compareEdgePattern = true;
                }

                // adjust the position of the tips down slightly, so that
                // standard geometry looks better
                tail_left_x2 = tail_left_x;
                tail_left_y2 = tail_left_y - ((tail_left_y - tail_axis_y) * 15 / 100);
                tail_right_x2 = tail_right_x;
                tail_right_y2 = tail_right_y - ((tail_right_y - tail_axis_y) * 15 / 100);

                showTailGrid();

                createTailImage();

                // find interesting features on the tail
                locateKeyFeatures();

                // find linear features, such as scars
                locateLinearFeatures();

                // reduce the number of features down to some managable amount by non-maximal supression
                nonmax_supress(key_features, 30);
                nonmax_supress(LinearFeaturesAll, 30);

                // update geometric properties of features on the tail
                updateGeometry(key_features);
                updateGeometry(LinearFeaturesAll);

                // show key features
                showKeyFeatures(tail_features, tail_image_width, tail_image_height);

                picTailStandard.Visible = true;
                if (percentCoverage > min_percent_coverage)
                {
                    segmentation_ok = true;
                }
            }

        }

        private Image ConvertImageTo24bpp(Image img)
        {
            Bitmap pic = (Bitmap)img;
            //MessageBox.Show(img.PixelFormat.ToString());
            Bitmap pic24 = new Bitmap(pic.Width, pic.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
            Graphics g = Graphics.FromImage(pic24);
            //g.DrawImageUnscaled(pic, 0, 0);
            g.DrawImage(pic, 0, 0);
            g.Dispose();

            return (pic24);
        }


        private bool loadTail(String filename)
        {
            bool success = false;

            if (File.Exists(filename))
            {
                ImageFilename = filename;
                tabWhaleSpotter.SelectedTab = tabNewSighting;

                // get the creation date of the file
                DateTime fileDate = File.GetCreationTime(filename);
                SightingDate.Value = fileDate;
                cmbHour.Text = Convert.ToString(fileDate.Hour);
                cmbMin.Text = Convert.ToString(fileDate.Minute);

                try
                {
                    picSighting.Load(filename);
                    success = true;
                }
                catch
                {
                }
                //picSighting.Image = ConvertImageTo24bpp(picSighting.Image);
                picManual.Image = picSighting.Image;                

                if (success)
                {
                    if (!manual_keypoints) manual_points = new ArrayList();
                    manual_keypoints = false;

                    picTailStandard.Visible = false;
                    picSighting.Visible = true;
                    picSighting.Refresh();

                    image_width = picSighting.Image.Width;
                    image_height = picSighting.Image.Height;
                    
                    // update the raw image
                    Byte[] original_image = new Byte[image_width * image_height * 3];
                    BitmapArrayConversions.updatebitmap((Bitmap)picSighting.Image, original_image);
                    
                    // make the stride even
                    raw_image = original_image;

                    Stride = image_width*3;

                    picWhale.Image = new Bitmap(image_width, image_height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                    //BitmapArrayConversions.updatebitmap((Bitmap)picWhale.Image, raw_image);

                    update();

                    picForeground.Image = new Bitmap(image_width, image_height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                    BitmapArrayConversions.updatebitmap_unsafe(foreground, (Bitmap)(picForeground.Image));

                    picTailStandard.Image = new Bitmap(tail_image_width, tail_image_height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                    picOriginal.Image = new Bitmap(tail_image_width, tail_image_height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                    BitmapArrayConversions.updatebitmap_unsafe(tail_image, (Bitmap)(picTailStandard.Image));
                    BitmapArrayConversions.updatebitmap_unsafe(tail_image, (Bitmap)(picOriginal.Image));

                    picTailFeatures.Image = new Bitmap(tail_image_width, tail_image_height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                    BitmapArrayConversions.updatebitmap_unsafe(tail_features, (Bitmap)(picTailFeatures.Image));

                    picEdges.Image = new Bitmap(tail_image_width, tail_image_height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                    picOriginalEdges.Image = new Bitmap(tail_image_width, tail_image_height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                    BitmapArrayConversions.updatebitmap_unsafe(edges_image, (Bitmap)(picEdges.Image));
                    BitmapArrayConversions.updatebitmap_unsafe(edges_image, (Bitmap)(picOriginalEdges.Image));

                    BitmapArrayConversions.updatebitmap_unsafe(keypoints, (Bitmap)(picWhale.Image));
                    picWhale.Refresh();

                }

            }
            return (success);
        }


        #endregion

        #region "form based stuff"

        public frmMain()
        {
            InitializeComponent();

            String path = base_path;  // change this if needed
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);

            tailPath = path + "tails\\";
            if (!Directory.Exists(tailPath))
                Directory.CreateDirectory(tailPath);

            maps_filename = base_path + "maps.dat";
            database_filename = base_path + "database.dat";
            user_settings_filename = base_path + "settings.dat";

            createSpeciesCombo();
            database.Load(database_filename);
            maps.Load(maps_filename);
            userSettings.Load(user_settings_filename);
            showDefaultMap();
            createIndividualNames();
            createIndividualNamesSearch();
            updateStatistics();

            txtRecognitionThreshold.Text = Convert.ToString(userSettings.RecognitionThreshold);
            txtWeightPointFeatures.Text = Convert.ToString(userSettings.weight_key_features);
            txtWeightLinearFeatures.Text = Convert.ToString(userSettings.weight_linear_features);
            txtWeightEdgeFeatures.Text = Convert.ToString(userSettings.weight_edge_pattern);
            setRecognitionThreshold();
            txtCameraDrive.Text = userSettings.camera_drive;
        }


        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        private void NewSightingFromFile(String filename)
        {
            loadTail(filename);
            clearData();
            cmbObserver.Focus();
            useForIdentifications = false;
            if (segmentation_ok)
            {
                useForIdentifications = true;
                SearchDatabase();
            }
        }

        private void NewSighting()
        {
            String path = base_path + "sightings\\";  // change this directory as needed
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);

            openFileDialog1.Title = "Open Whale Tail Image";
            openFileDialog1.InitialDirectory = userSettings.camera_drive;   
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                NewSightingFromFile(openFileDialog1.FileName);
            }
        }

        private void loadWhaleToolStripMenuItem_Click(object sender, EventArgs e)
        {
            NewSighting();
        }

        private void picProfiles_Click(object sender, EventArgs e)
        {

        }

        private void createSpeciesCombo()
        {
            String[] species = {
                "Sperm Whale, Physeter macrocephalus",
                "Dwarf Sperm Whale, Kogia sima",
                "Pygmy Sperm Whale, Kogia breviceps",
                "Cuvier's Beaked Whale, Ziphius cavirostris",
                "Arnoux's Beaked Whale, Berardius arnuxii",
                "Baird's Beaked Whale (North Pacific Bottlenose Whale), Berardius bairdii",
                "Shepherd's Beaked Whale (Tasman Beaked Whale), Tasmacetus shepherdi",
                "Longman's Beaked Whale (Indo-Pacific Beaked Whale), Indopacetus pacificus",
                "Northern Bottlenose Whale, Hyperoodon ampullatus",
                "Southern Bottlenose Whale, Hyperoodon planifrons",
                "Hector's Beaked Whale, Mesoplodon hectori",
                "True's Beaked Whale, Mesoplodon mirus",
                "Gervais' Beaked Whale, Mesoplodon europaeus",
                "Sowerby's Beaked Whale, Mesoplodon bidens",
                "Gray's Beaked Whale, Mesoplodon grayi",
                "Pygmy Beaked Whale, Mesoplodon peruvianus",
                "Andrews' Beaked Whale, Mesoplodon bowdoini",
                "Bahamonde's Beaked Whale, Mesoplodon bahamondi",
                "Hubbs' Beaked Whale, Mesoplodon carlhubbsi",
                "Ginkgo-toothed Beaked Whale, Mesoplodon ginkgodens",
                "Stejneger's Beaked Whale, Mesoplodon stejnegeri",
                "Layard's Beaked Whale, Mesoplodon layardii",
                "Blainville's Beaked Whale, Mesoplodon densirostris",
                "Perrin's Beaked Whale, Mesoplodon perrini",
                "Narwhal, Monodon monoceros",
                "Beluga, Delphinapterus leucas",    

                "Bowhead Whale, Balaena mysticetus",
                "Atlantic Northern Right Whale, Eubalaena glacialis",
                "Pacific Northern Right Whale, Eubalaena japonica",
                "Southern Right Whale, Eubalaena australia",
                "Fin Whale, Balaenoptera physalus",
                "Sei Whale, Balaenoptera borealis",
                "Bryde's Whale, Balaenoptera brydei",
                "Pygmy Bryde's Whale, (Eden's Whale) Balaenoptera edeni",
                "Blue Whale, Balaenoptera musculus",
                "Northern Minke Whale, Balaenoptera acutorostrata",
                "Southern Minke Whale, (Antarctic Minke Whale) Balaenoptera bonaerensis",
                "Balaenoptera omurai, discovery announced November 2003.",
                "Humpback Whale, Megaptera novaeangliae",
                "Gray Whale, Eschrichtius robustus",
                "Pygmy Right Whale, Caperea marginata"
            };

            cmbSpecies.Items.Clear();
            cmbMapSpecies.Items.Clear();
            cmbSearchSpecies.Items.Clear();
            cmbMapSpecies.Items.Add("All");
            cmbSearchSpecies.Items.Add("All");
            for (int i = 0; i < species.Length; i++)
            {
                cmbSpecies.Items.Add(species[i]);
                cmbMapSpecies.Items.Add(species[i]);
                cmbSearchSpecies.Items.Add(species[i]);
            }
            cmbMapSpecies.Text = "All";   
            cmbSearchSpecies.Text = "All";
        }


        /// <summary>
        /// populate the names combo
        /// </summary>
        private void createIndividualNames()
        {
            cmbMapIndividualName.Items.Clear();
            ArrayList names = database.getNamesBySpecies(cmbMapSpecies.Text);
            for (int i = 0; i < names.Count; i++)
            {
                cmbMapIndividualName.Items.Add((String)names[i]);
            }
        }

        private void createIndividualNamesSearch()
        {
            cmbSearchIndividualName.Items.Clear();
            ArrayList names = database.getNamesBySpecies(cmbSearchSpecies.Text);
            for (int i = 0; i < names.Count; i++)
            {
                cmbSearchIndividualName.Items.Add((String)names[i]);
            }          
        }

        private void clearData()
        {
            txtName.Text = "";
            txtSerialNumber.Text = "";
            cmbSex.Text = "Unknown";
            txtNotes.Text = "";
            grpSearchResults.Visible = false;
        }


        private void cmdStore_Click(object sender, EventArgs e)
        {
            useForIdentifications = segmentation_ok;
            AddToDatabase();
        }

        private void frmMain_FormClosing(object sender, FormClosingEventArgs e)
        {
            database.Save(database_filename);
            setWeights();
            userSettings.Save(user_settings_filename);
        }

        /// <summary>
        /// compare the selected result against the current sighting
        /// </summary>
        /// <param name="pic"></param>
        /// <param name="index"></param>
        private void compareResult(int index)
        {
            selected_search_result_index = index;

            whale w = (whale)search_results[index];

            grpCompare.Visible = true;
            String tailFilename = ((sighting)w.Sightings[0]).TailFilename;
            if (File.Exists(tailFilename))
            {
                picCompare.Load(tailFilename);

                int Texture2 = 0;
                Byte[] tail_image2 = new Byte[tail_image_width * tail_image_height * 3];
                Byte[] edges_image2 = new Byte[tail_image_width * tail_image_height * 3];
                BitmapArrayConversions.updatebitmap((Bitmap)picCompare.Image, tail_image2);
                picCompareEdges.Image = new Bitmap(tail_image_width, tail_image_height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                createEdgeImage(tail_image2, edges_image2, tail_image_width, tail_image_height, ref Texture2);
                BitmapArrayConversions.updatebitmap_unsafe(edges_image2, (Bitmap)(picCompareEdges.Image));

                Byte[] tail_edge1 = new Byte[EDGE_PATTERN_BINS * 10 * 10 * 3];
                w.showEdgePattern(tail_edge1, EDGE_PATTERN_BINS * 10, 10, findwhale.edge_pattern);
                picTrailingEdgePattern1.Image = new Bitmap(EDGE_PATTERN_BINS * 10, 10, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                BitmapArrayConversions.updatebitmap_unsafe(tail_edge1, (Bitmap)(picTrailingEdgePattern1.Image));
                Byte[] tail_edge2 = new Byte[EDGE_PATTERN_BINS * 10 * 10 * 3];
                findwhale.showEdgePattern(tail_edge2, EDGE_PATTERN_BINS * 10, 10, w.edge_pattern);
                picTrailingEdgePattern2.Image = new Bitmap(EDGE_PATTERN_BINS * 10, 10, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                BitmapArrayConversions.updatebitmap_unsafe(tail_edge2, (Bitmap)(picTrailingEdgePattern2.Image));

                tabWhaleSpotter.SelectedTab = tabCompare;
            }
        }

        private void picSearchResult0_Click(object sender, EventArgs e)
        {
            compareResult(0);
        }

        private void picSearchResult1_Click(object sender, EventArgs e)
        {
            compareResult(1);
        }

        private void picSearchResult2_Click(object sender, EventArgs e)
        {
            compareResult(2);
        }

        private void picSearchResult3_Click(object sender, EventArgs e)
        {
            compareResult(3);
        }

        private void cmdDifferent_Click(object sender, EventArgs e)
        {
            tabWhaleSpotter.SelectedTab = tabNewSighting;
            txtName.Text = "";
            txtSerialNumber.Text = "";
            txtName.Focus();
        }

        private void cmdSame_Click(object sender, EventArgs e)
        {
            if (selected_search_result_index > -1)
            {
                whale w = (whale)search_results[selected_search_result_index];
                tabWhaleSpotter.SelectedTab = tabNewSighting;
                cmbSpecies.Text = w.Species;
                txtName.Text = w.Name;
                txtSerialNumber.Text = w.SerialNumber;
            }
        }

        private void cmdNewSighting_Click(object sender, EventArgs e)
        {
            NewSighting();
        }

        /// <summary>
        /// populate the list of map descriptions
        /// </summary>
        private void populateMaps()
        {
            cmbMap.Items.Clear();
            ArrayList descriptions = maps.getDescriptions();
            for (int i = 0; i < descriptions.Count; i++)
            {
                cmbMap.Items.Add((String)descriptions[i]);
            }

            String description = "";
            String top_longitude = "";
            String top_lattitude = "";
            String bottom_longitide = "";
            String bottom_lattitude = "";
            maps.getMapParameters(current_map_filename, ref description,
                                  ref top_longitude, ref top_lattitude,
                                  ref bottom_longitide, ref bottom_lattitude);
            cmbMap.Text = description;
            txtTopLongitude.Text = top_longitude;
            txtTopLattitude.Text = top_lattitude;
            txtBottomLongitude.Text = bottom_longitide;
            txtBottomLattitude.Text = bottom_lattitude;
        }

        /// <summary>
        /// update the parameters for this map
        /// </summary>
        private void updateMapParameters()
        {
            if (current_map_filename != "")
            {
                String top_longitude="", top_lattitude="", bottom_longitide="", bottom_lattitude="";
                String description = "";

                if (cmbMap.Text != "")
                    description = cmbMap.Text;

                if (txtTopLongitude.Text != "")
                    top_longitude = txtTopLongitude.Text;

                if (txtTopLattitude.Text != "")
                    top_lattitude = txtTopLattitude.Text;

                if (txtBottomLongitude.Text != "")
                    bottom_longitide = txtBottomLongitude.Text;

                if (txtBottomLattitude.Text != "")
                    bottom_lattitude = txtBottomLattitude.Text;

                maps.setMapParameters(current_map_filename, description, top_longitude,
                                      top_lattitude, bottom_longitide, bottom_lattitude);

                maps.Save(maps_filename);
            }
        }

        /// <summary>
        /// load a map from file
        /// </summary>
        private void LoadMap()
        {
            String path = base_path + "maps\\";  // change this directory as needed
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);

            openFileDialog1.Title = "Open Map Image";
            openFileDialog1.FileName = "";
            openFileDialog1.InitialDirectory = path;
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                maps.Save(maps_filename);
                String filename = openFileDialog1.FileName;
                maps.Add(filename);
                picMap.Load(filename);
                map_image = new Byte[picMap.Image.Width * picMap.Image.Height * 3];
                BitmapArrayConversions.updatebitmap((Bitmap)picMap.Image, map_image);
                map_image_width = picMap.Image.Width;
                map_image_height = picMap.Image.Height;
                maps.Save(maps_filename);
                populateMaps();
                cmbMap.Text = maps.getMapDescription(filename);
                showMapBySpecies(cmbMapSpecies.Text, "All");
                tabWhaleSpotter.SelectedTab = tabMap;

                current_map_filename = filename;
            }
        }

        private void loadMapToolStripMenuItem_Click(object sender, EventArgs e)
        {
            LoadMap();
        }

        private void tabMap_Click(object sender, EventArgs e)
        {

        }

        private void deleteToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (tabWhaleSpotter.SelectedTab == tabMap)
            {
                maps.Remove(current_map_filename);
                showDefaultMap();
            }
        }

        /// <summary>
        /// export the data to a spreadsheet
        /// </summary>
        private void exportToSpreadsheet()
        {
            database.ExportToCSV(spreadsheet_export_filename);

            System.Diagnostics.Process proc = new System.Diagnostics.Process();
            proc.EnableRaisingEvents = false;
            proc.StartInfo.FileName = spreadsheet_export_filename;
            try
            {
                proc.Start();
            }
            catch
            {
            }
        }

        private void showDefaultMap()
        {
            current_map_filename = maps.getMapFilename(0);
            if (current_map_filename != "")
            {
                try
                {
                    picMap.Load(current_map_filename);
                    picMap.Refresh();
                }
                catch
                {
                }
            }
            populateMaps();
        }

        /// <summary>
        /// select a map from the combo
        /// </summary>
        private void selectMap()
        {
            if (cmbMap.Text != "")
            {
                current_map_filename = maps.getMapFilename(cmbMap.Text);
                if (prev_map_filename != current_map_filename)
                {
                    prev_map_filename = current_map_filename;
                    picMap.Load(current_map_filename);
                    map_image = new Byte[picMap.Image.Width * picMap.Image.Height * 3];
                    BitmapArrayConversions.updatebitmap((Bitmap)picMap.Image, map_image);
                    map_image_width = picMap.Image.Width;
                    map_image_height = picMap.Image.Height;
                    picMap.Refresh();
                    populateMaps();
                    showMapBySpecies(cmbMapSpecies.Text, "All");
                }
            }
        }

        private void cmbMap_SelectedIndexChanged(object sender, EventArgs e)
        {
            selectMap();
        }

        private void exportToSpreadsheetToolStripMenuItem_Click(object sender, EventArgs e)
        {
            exportToSpreadsheet();
        }

        private void txtTopLongitude_TextChanged(object sender, EventArgs e)
        {
            updateMapParameters();
        }

        private void txtTopLattitude_CausesValidationChanged(object sender, EventArgs e)
        {
            
        }

        private void txtTopLattitude_Leave(object sender, EventArgs e)
        {
            updateMapParameters();
        }

        private void txtTopLattitude_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 13) updateMapParameters();
        }

        private void txtTopLongitude_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 13) updateMapParameters();
        }

        private void txtBottomLongitude_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 13) updateMapParameters();
        }

        private void txtBottomLattitude_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 13) updateMapParameters();
        }

        private void txtTopLongitude_Leave(object sender, EventArgs e)
        {
            updateMapParameters();
        }

        private void txtBottomLongitude_Leave(object sender, EventArgs e)
        {
            updateMapParameters();
        }

        private void txtBottomLattitude_Leave(object sender, EventArgs e)
        {
            updateMapParameters();
        }

        private void cmbMap_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 13) updateMapParameters();
        }

        private void cmbMap_Leave(object sender, EventArgs e)
        {
            updateMapParameters();
        }

        private void cmbMapSpecies_SelectedIndexChanged(object sender, EventArgs e)
        {
            createIndividualNames();
            cmbMapIndividualName.Text = "";
            showMapBySpecies(cmbMapSpecies.Text, "All");
        }

        private void cmbMapIndividualName_SelectedIndexChanged(object sender, EventArgs e)
        {
            showMapBySpecies(cmbMapSpecies.Text, cmbMapIndividualName.Text);
        }

        private void toolStripButton1_Click(object sender, EventArgs e)
        {
            NewSighting();
        }

        private void toolStripButtonAddMap_Click(object sender, EventArgs e)
        {
            LoadMap();
        }

        private void cmbSearchSpecies_SelectedIndexChanged(object sender, EventArgs e)
        {
            createIndividualNamesSearch();
            cmbSearchIndividualName.Text = "";
            createSearchResultsList(lstSearchResults, cmbSearchSpecies.Text, "All");
        }

        private void createSearchResultsList(ListView lst, String species, String individualName)
        {
            if (individualName == "") individualName = "All";

            if (((chkSearchRadius.Checked) &&
                ((((!txtSearchRadiusLongitude.Text.Contains("E")) && (!txtSearchRadiusLongitude.Text.Contains("W"))) ||
                ((!txtSearchRadiusLattitude.Text.Contains("N")) && (!txtSearchRadiusLattitude.Text.Contains("S"))))
                )))
            {
                MessageBox.Show("Please enter valid lattitude and longitude values");
            }
            else
            {
                lst.Items.Clear();
                lst.Columns.Clear();

                //lst.Dock = DockStyle.Fill;
                lst.View = View.Details;
                lst.Sorting = SortOrder.Descending;

                // Create and initialize column headers for myListView.
                ColumnHeader columnHeader0 = new ColumnHeader();
                columnHeader0.Text = "Date";
                columnHeader0.Width = 80;
                ColumnHeader columnHeader1 = new ColumnHeader();
                columnHeader1.Text = "Name";
                columnHeader1.Width = 150;
                ColumnHeader columnHeader2 = new ColumnHeader();
                columnHeader2.Text = "Serial No.";
                columnHeader2.Width = 100;
                ColumnHeader columnHeader3 = new ColumnHeader();
                columnHeader3.Text = "Notes";
                columnHeader3.Width = 600;
                ColumnHeader columnHeaderImg1 = new ColumnHeader();
                columnHeaderImg1.Text = "Tail Image";
                columnHeaderImg1.Width = 0;
                ColumnHeader columnHeaderImg2 = new ColumnHeader();
                columnHeaderImg2.Text = "Sighting Image";
                columnHeaderImg2.Width = 0;

                // Add the column headers to myListView.
                lst.Columns.AddRange(new ColumnHeader[] { columnHeader0, columnHeader1, columnHeader2, columnHeader3,
                                                          columnHeaderImg1, columnHeaderImg2  });

                float x = 0, y = 0;
                float searchRadius_nm = 0;
                if (chkSearchRadius.Checked)
                {
                    x = util.MapToFloat(txtSearchRadiusLongitude.Text);
                    y = util.MapToFloat(txtSearchRadiusLattitude.Text);
                    searchRadius_nm = Convert.ToSingle(txtSearchRadius.Text);
                }                

                ArrayList sightings = database.getSightingsBySpecies(species, individualName);
                for (int i = 0; i < sightings.Count; i++)
                {
                    sighting s = (sighting)sightings[i];
                    if ((s.individual.Species == species) || (species == "All"))
                    {
                        if ((s.individual.Name == individualName) || (individualName == "All"))
                        {
                            bool withinSearchRadius = false;

                            if (chkSearchRadius.Checked)
                            {
                                float dx_nm = (s.longitude - x) * 60;
                                dx_nm = dx_nm * (((float)Math.Cos(y / 180.0f * Math.PI) + 1) / 2.0f);
                                float dy_nm = (s.lattitude - y) * 60;
                                float dist_nm = (float)Math.Sqrt((dx_nm * dx_nm) + (dy_nm * dy_nm));
                                if (dist_nm < searchRadius_nm)
                                    withinSearchRadius = true;
                            }
                            else withinSearchRadius = true;

                            if (withinSearchRadius)
                            {
                                String DateStr = Convert.ToString(s.date.Year) + "/" +
                                                 Convert.ToString(s.date.Month) + "/" +
                                                 Convert.ToString(s.date.Day);

                                ListViewItem result = new ListViewItem(new string[] 
                                    {DateStr, 
                                     s.individual.Name, 
                                     s.individual.SerialNumber,
                                     s.Notes,
                                     s.TailFilename,
                                     s.ImageFilename});

                                lst.Items.Add(result);
                            }
                        }
                    }

                }


            }
        }

        private void chkSearchRadius_CheckedChanged(object sender, EventArgs e)
        {
            createSearchResultsList(lstSearchResults, cmbSearchSpecies.Text, "All");
        }

        private void txtSearchRadiusLongitude_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 13)
                createSearchResultsList(lstSearchResults, cmbSearchSpecies.Text, cmbSearchIndividualName.Text);
        }

        private void txtSearchRadiusLattitude_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 13)
                createSearchResultsList(lstSearchResults, cmbSearchSpecies.Text, cmbSearchIndividualName.Text);
        }

        private void txtSearchRadius_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 13)
                createSearchResultsList(lstSearchResults, cmbSearchSpecies.Text, cmbSearchIndividualName.Text);
        }

        private void cmbSearchIndividualName_SelectedIndexChanged(object sender, EventArgs e)
        {
            createSearchResultsList(lstSearchResults, cmbSearchSpecies.Text, cmbSearchIndividualName.Text);
        }

        private void lstSearchResults_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (lstSearchResults.SelectedIndices.Count > 0)
            {
                int idx = lstSearchResults.SelectedIndices[0];                
                ListViewItem.ListViewSubItem sighting_filename = lstSearchResults.Items[idx].SubItems[5];
                current_sighting_filename = sighting_filename.Text;
                picSearchResult.Load(current_sighting_filename);                
            }
        }

        private void picSearchResult_Click(object sender, EventArgs e)
        {
            tabWhaleSpotter.SelectedTab = tabNewSighting;
            NewSightingFromFile(current_sighting_filename);
        }

        /// <summary>
        /// set the recognition threshold
        /// </summary>
        private void setRecognitionThreshold()
        {
            int percent = 50;
            if (txtRecognitionThreshold.Text != "")
                percent = Convert.ToInt32(txtRecognitionThreshold.Text);

            // check range
            if (percent < 0)
            {
                percent = 0;
                txtRecognitionThreshold.Text = Convert.ToString(percent);
            }

            if (percent > 100)
            {
                percent = 100;
                txtRecognitionThreshold.Text = Convert.ToString(percent);
            }

            userSettings.RecognitionThreshold = percent;
            database.setRecognitionThreshold(percent);
        }


        private void txtRecognitionThreshold_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 13)
            {
                setRecognitionThreshold();
            }
        }

        private void txtRecognitionThreshold_Leave(object sender, EventArgs e)
        {
            setRecognitionThreshold();
        }

        private void cmdRecognitionThresholdIncrease_Click(object sender, EventArgs e)
        {
            int percent = Convert.ToInt32(txtRecognitionThreshold.Text);
            txtRecognitionThreshold.Text = Convert.ToString(percent + 1);
            setRecognitionThreshold();
        }

        private void cmdRecognitionThresholdDecrease_Click(object sender, EventArgs e)
        {
            int percent = Convert.ToInt32(txtRecognitionThreshold.Text);
            txtRecognitionThreshold.Text = Convert.ToString(percent - 1);
            setRecognitionThreshold();
        }


        private void setWeights()
        {
            if (txtWeightPointFeatures.Text != "")
            {
                database.weight_key_features = Convert.ToSingle(txtWeightPointFeatures.Text);
                userSettings.weight_key_features = database.weight_key_features;
            }

            if (txtWeightLinearFeatures.Text != "")
            {
                database.weight_linear_features = Convert.ToSingle(txtWeightLinearFeatures.Text);
                userSettings.weight_linear_features = database.weight_linear_features;
            }

            if (txtWeightEdgeFeatures.Text != "")
            {
                database.weight_edge_pattern = Convert.ToSingle(txtWeightEdgeFeatures.Text);
                userSettings.weight_edge_pattern = database.weight_edge_pattern;
            }
        }

        #endregion

        private void txtWeightPointFeatures_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 13) setWeights();
        }

        private void txtWeightLinearFeatures_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 13) setWeights();
        }

        private void txtWeightEdgeFeatures_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == 13) setWeights();
        }

        private void txtWeightPointFeatures_Leave(object sender, EventArgs e)
        {
            setWeights();
        }

        private void txtWeightLinearFeatures_Leave(object sender, EventArgs e)
        {
            setWeights();
        }

        private void txtWeightEdgeFeatures_Leave(object sender, EventArgs e)
        {
            setWeights();
        }

        private void picManual_MouseMove(object sender, MouseEventArgs e)
        {
            LocalMousePosition = picManual.PointToClient(Cursor.Position);
        }

        private void showManualPoints()
        {
            Pen p;
            SolidBrush brush;
            Rectangle rect;
            int i, radius_x, radius_y, x, y;
            Graphics gr;

            if (picManual.Image != null)
            {
                picManual.Image = (Image)picSighting.Image.Clone();

                gr = Graphics.FromImage(picManual.Image);
                rect = new Rectangle();

                // make a brush with some alpha blending
                brush = new SolidBrush(Color.FromArgb(120, 240, 0, 0));
                p = new Pen(brush);

                for (i = 0; i < manual_points.Count; i++)
                {
                    tailPoint pt = (tailPoint)manual_points[i];

                    // get the position and disparity for this feature
                    x = pt.x;
                    y = pt.y;

                    // circle radii
                    radius_x = picManual.Image.Width / 60;
                    radius_y = picManual.Image.Height / 60;

                    // the rectangle of doom
                    rect.X = x - radius_x;
                    rect.Y = y - radius_y;
                    rect.Width = radius_x * 2;
                    rect.Height = radius_y * 2;

                    // draw the circle
                    gr.FillEllipse(brush, rect);
                }
                picManual.Refresh();
            }

        }

        /// <summary>
        /// manually select points on the tail
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void picManual_Click(object sender, EventArgs e)
        {
            if (picManual.Image != null)
            {
                int x = LocalMousePosition.X * image_width / picManual.Width;
                int y = LocalMousePosition.Y * image_height / picManual.Height;

                if (x < 5) x = 5;
                if (x > image_width - 5) x = image_width - 5;
                if (y < 5) y = 5;
                if (y > image_height - 5) y = image_height - 5;

                int search_x = picManual.Image.Width / 20;
                int search_y = picManual.Image.Height / 20;

                int i = 0;
                bool found = false;
                while ((i < manual_points.Count) && (!found))
                {
                    tailPoint pt = (tailPoint)manual_points[i];
                    int dx = pt.x - x;
                    if (dx < 0) dx = -dx;
                    int dy = pt.y - y;
                    if (dy < 0) dy = -dy;
                    if ((dx < search_x) && (dy < search_y))
                    {
                        pt.x = x;
                        pt.y = y;
                        found = true;
                    }
                    i++;
                }
                if ((!found) && (manual_points.Count < 5))
                {
                    tailPoint new_pt = new tailPoint(x, y);
                    manual_points.Add(new_pt);
                }
                showManualPoints();
            }
        }

        private void cmdClearManualPoints_Click(object sender, EventArgs e)
        {
            manual_points.Clear();
            if (picManual.Image != null)
            {
                showManualPoints();
            }
        }

        private void cmdManualUpdate_Click(object sender, EventArgs e)
        {
            manual_keypoints = true;
            loadTail(ImageFilename);
            showManualPoints();
        }

        private void cmdCameraDrive_Click(object sender, EventArgs e)
        {
            openFileDialog1.DefaultExt = "jpg";
            openFileDialog1.Filter = "Jpeg files (*.jpg)|*.jpg";
            folderBrowserDialog1.Description = "Select the directory where camera images are located";
            folderBrowserDialog1.ShowNewFolderButton = false;

            DialogResult result = folderBrowserDialog1.ShowDialog();
            if (result == DialogResult.OK)
            {
                txtCameraDrive.Text = folderBrowserDialog1.SelectedPath;
                userSettings.camera_drive = txtCameraDrive.Text;
                userSettings.Save(user_settings_filename);
            }
        }

        private void txtCameraDrive_Leave(object sender, EventArgs e)
        {
            userSettings.Save(user_settings_filename);
        }

        private void cmbHeading_SelectedIndexChanged(object sender, EventArgs e)
        {

        }
    }
}